feat: 日期检查器 UI 改版,添加过期状态显示
- 生产日期改为渐变卡片展示,支持点击切换日期 - 保质期条目改为 ExpiryCard,显示过期状态标签 - 新增 ExpiryStatus 枚举(SAFE/WARNING/URGENT/EXPIRED) - 新增 ArrowRightIcon 自定义图标 - 添加中文日期格式化辅助函数 - 改进 TopAppBar 和 FAB 样式 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
23909e855f
commit
b12206dc88
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
package plus.rua.project.ui
|
package plus.rua.project.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@ -23,7 +25,6 @@ import androidx.compose.material3.DatePicker
|
|||||||
import androidx.compose.material3.DatePickerDialog
|
import androidx.compose.material3.DatePickerDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@ -33,6 +34,7 @@ import androidx.compose.material3.SwipeToDismissBoxValue
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberDatePickerState
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -45,11 +47,13 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -71,11 +75,33 @@ private sealed class DatePickerTarget {
|
|||||||
data class Row(val rowId: Int) : DatePickerTarget()
|
data class Row(val rowId: Int) : DatePickerTarget()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class ExpiryStatus {
|
||||||
|
UNKNOWN, SAFE, WARNING, URGENT, EXPIRED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExpiryStatus.color(): Color = when (this) {
|
||||||
|
ExpiryStatus.SAFE -> Color(0xFF059669)
|
||||||
|
ExpiryStatus.WARNING -> Color(0xFFD97706)
|
||||||
|
ExpiryStatus.URGENT -> Color(0xFFEA580C)
|
||||||
|
ExpiryStatus.EXPIRED -> Color(0xFFDC2626)
|
||||||
|
ExpiryStatus.UNKNOWN -> MaterialTheme.colorScheme.outline
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExpiryStatus.containerColor(): Color = when (this) {
|
||||||
|
ExpiryStatus.SAFE -> Color(0xFFD1FAE5)
|
||||||
|
ExpiryStatus.WARNING -> Color(0xFFFEF3C7)
|
||||||
|
ExpiryStatus.URGENT -> Color(0xFFFFEDD5)
|
||||||
|
ExpiryStatus.EXPIRED -> Color(0xFFFEE2E2)
|
||||||
|
ExpiryStatus.UNKNOWN -> MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日期检查器页面,商品过期检查工具。
|
* 日期检查器页面,商品过期检查工具。
|
||||||
*
|
*
|
||||||
* 顶部设置生产日期,下方多行显示天数与到期日期的双向联动计算。
|
* 顶部设置生产日期,下方多行显示天数与到期日期的双向联动计算。
|
||||||
* 支持滑动删除行、FAB 添加新行。
|
* 支持删除行、FAB 添加新行,并显示每条保质期的过期状态。
|
||||||
*
|
*
|
||||||
* @param onBack 返回回调
|
* @param onBack 返回回调
|
||||||
* @param modifier 布局修饰符
|
* @param modifier 布局修饰符
|
||||||
@ -103,12 +129,22 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("日期检查器") },
|
title = {
|
||||||
|
Text(
|
||||||
|
"日期检查器",
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
BackArrowIcon()
|
BackArrowIcon()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@ -119,52 +155,105 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
},
|
},
|
||||||
modifier = Modifier.testTag("date_checker_fab"),
|
modifier = Modifier.testTag("date_checker_fab"),
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
) {
|
) {
|
||||||
PlusIcon(color = MaterialTheme.colorScheme.onPrimaryContainer)
|
PlusIcon(color = MaterialTheme.colorScheme.onPrimary)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
ProductionDateCard(
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "生产日期",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
ProductionDateField(
|
|
||||||
date = productionDate,
|
date = productionDate,
|
||||||
onDateChange = { productionDate = it },
|
isToday = productionDate == today,
|
||||||
onShowDatePicker = {
|
onClick = {
|
||||||
datePickerTarget = DatePickerTarget.Production
|
datePickerTarget = DatePickerTarget.Production
|
||||||
showDatePicker = true
|
showDatePicker = true
|
||||||
}
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
HorizontalDivider()
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "保质期列表",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${rows.size} 项",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
items(rows, key = { it.id }) { row ->
|
items(rows, key = { it.id }) { row ->
|
||||||
val expiryDate = row.days?.let { productionDate.plus(DatePeriod(days = it)) }
|
val expiryDate = row.days?.let { productionDate.plus(DatePeriod(days = it)) }
|
||||||
|
val daysRemaining = expiryDate?.let { today.daysUntil(it) }
|
||||||
|
val status = when {
|
||||||
|
daysRemaining == null -> ExpiryStatus.UNKNOWN
|
||||||
|
daysRemaining < 0 -> ExpiryStatus.EXPIRED
|
||||||
|
daysRemaining == 0 -> ExpiryStatus.URGENT
|
||||||
|
daysRemaining <= 7 -> ExpiryStatus.URGENT
|
||||||
|
daysRemaining <= 30 -> ExpiryStatus.WARNING
|
||||||
|
else -> ExpiryStatus.SAFE
|
||||||
|
}
|
||||||
|
|
||||||
ExpiryRowItem(
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
|
confirmValueChange = { value ->
|
||||||
|
if (value == SwipeToDismissBoxValue.EndToStart) {
|
||||||
|
rows = rows.filter { it.id != row.id }
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwipeToDismissBox(
|
||||||
|
state = dismissState,
|
||||||
|
backgroundContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.errorContainer)
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "删除",
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ExpiryCard(
|
||||||
days = row.days,
|
days = row.days,
|
||||||
expiryDate = expiryDate,
|
expiryDate = expiryDate,
|
||||||
|
daysRemaining = daysRemaining,
|
||||||
|
status = status,
|
||||||
onDaysChange = { newDays ->
|
onDaysChange = { newDays ->
|
||||||
rows = rows.map {
|
rows = rows.map {
|
||||||
if (it.id == row.id) it.copy(days = newDays) else it
|
if (it.id == row.id) it.copy(days = newDays) else it
|
||||||
@ -179,15 +268,13 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
onShowDatePicker = {
|
onShowDatePicker = {
|
||||||
datePickerTarget = DatePickerTarget.Row(row.id)
|
datePickerTarget = DatePickerTarget.Row(row.id)
|
||||||
showDatePicker = true
|
showDatePicker = true
|
||||||
},
|
|
||||||
onDelete = {
|
|
||||||
rows = rows.filter { it.id != row.id }
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showDatePicker) {
|
if (showDatePicker) {
|
||||||
val initialMillis = when (val target = datePickerTarget) {
|
val initialMillis = when (val target = datePickerTarget) {
|
||||||
@ -237,102 +324,113 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProductionDateField(
|
private fun ProductionDateCard(
|
||||||
date: LocalDate,
|
date: LocalDate,
|
||||||
onDateChange: (LocalDate) -> Unit,
|
isToday: Boolean,
|
||||||
onShowDatePicker: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var text by remember(date) { mutableStateOf(date.toString()) }
|
val gradient = Brush.linearGradient(
|
||||||
var isError by remember { mutableStateOf(false) }
|
colors = listOf(
|
||||||
|
MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
Box(
|
||||||
value = text,
|
modifier = modifier
|
||||||
onValueChange = {
|
.fillMaxWidth()
|
||||||
text = it
|
.clip(RoundedCornerShape(20.dp))
|
||||||
isError = false
|
.background(gradient)
|
||||||
try {
|
.clickable(onClick = onClick)
|
||||||
onDateChange(LocalDate.parse(it))
|
.padding(20.dp)
|
||||||
} catch (_: Exception) {
|
|
||||||
isError = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text("生产日期") },
|
|
||||||
isError = isError,
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.Text,
|
|
||||||
imeAction = ImeAction.Done
|
|
||||||
),
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onShowDatePicker,
|
|
||||||
modifier = Modifier.testTag("date_picker_button")
|
|
||||||
) {
|
) {
|
||||||
CalendarIcon(color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Row(
|
||||||
}
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
},
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
modifier = modifier.fillMaxWidth()
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.12f)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CalendarIcon(
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "生产日期",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = date.formatChinese(),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
if (isToday) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = "今天 · ${date.dayOfWeekChinese()}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = date.dayOfWeekChinese(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExpiryRowItem(
|
private fun ExpiryCard(
|
||||||
days: Int?,
|
days: Int?,
|
||||||
expiryDate: LocalDate?,
|
expiryDate: LocalDate?,
|
||||||
|
daysRemaining: Int?,
|
||||||
|
status: ExpiryStatus,
|
||||||
onDaysChange: (Int?) -> Unit,
|
onDaysChange: (Int?) -> Unit,
|
||||||
onExpiryDateChange: (LocalDate) -> Unit,
|
onExpiryDateChange: (LocalDate) -> Unit,
|
||||||
onShowDatePicker: () -> Unit,
|
onShowDatePicker: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var daysText by remember(days) { mutableStateOf(days?.toString() ?: "") }
|
var daysText by remember(days) { mutableStateOf(days?.toString() ?: "") }
|
||||||
var dateText by remember(expiryDate) { mutableStateOf(expiryDate?.toString() ?: "") }
|
var dateText by remember(expiryDate) { mutableStateOf(expiryDate?.toString() ?: "") }
|
||||||
|
|
||||||
val dismissState = rememberSwipeToDismissBoxState(
|
|
||||||
confirmValueChange = { value ->
|
|
||||||
if (value == SwipeToDismissBoxValue.EndToStart) {
|
|
||||||
onDelete()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
SwipeToDismissBox(
|
|
||||||
state = dismissState,
|
|
||||||
backgroundContent = {
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.errorContainer)
|
|
||||||
.padding(horizontal = 20.dp),
|
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "删除",
|
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
|
||||||
style = MaterialTheme.typography.labelLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.padding(16.dp)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
) {
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = daysText,
|
value = daysText,
|
||||||
onValueChange = { newValue ->
|
onValueChange = { newValue ->
|
||||||
daysText = newValue.filter { it.isDigit() }
|
daysText = newValue.filter { it.isDigit() }.take(4)
|
||||||
onDaysChange(daysText.toIntOrNull())
|
onDaysChange(daysText.toIntOrNull())
|
||||||
},
|
},
|
||||||
label = { Text("天数") },
|
label = { Text("天数") },
|
||||||
@ -341,9 +439,19 @@ private fun ExpiryRowItem(
|
|||||||
keyboardType = KeyboardType.Number,
|
keyboardType = KeyboardType.Number,
|
||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
ArrowRightIcon(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = dateText,
|
value = dateText,
|
||||||
onValueChange = { newValue ->
|
onValueChange = { newValue ->
|
||||||
@ -360,16 +468,59 @@ private fun ExpiryRowItem(
|
|||||||
imeAction = ImeAction.Done
|
imeAction = ImeAction.Done
|
||||||
),
|
),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(onClick = onShowDatePicker) {
|
IconButton(
|
||||||
|
onClick = onShowDatePicker,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
) {
|
||||||
CalendarIcon(
|
CalendarIcon(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1.5f)
|
modifier = Modifier.weight(1.8f),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (daysRemaining != null) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
val statusText = when {
|
||||||
|
daysRemaining < 0 -> "已过期 ${-daysRemaining} 天"
|
||||||
|
daysRemaining == 0 -> "今天过期"
|
||||||
|
daysRemaining == 1 -> "明天过期"
|
||||||
|
else -> "还有 $daysRemaining 天"
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(status.containerColor())
|
||||||
|
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = statusText,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = status.color(),
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(8.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(status.color())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +529,7 @@ private fun ExpiryRowItem(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun BackArrowIcon(modifier: Modifier = Modifier) {
|
private fun BackArrowIcon(modifier: Modifier = Modifier) {
|
||||||
val color = MaterialTheme.colorScheme.onSurface
|
val color = MaterialTheme.colorScheme.onSurface
|
||||||
Canvas(modifier = modifier.size(24.dp)) {
|
androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) {
|
||||||
val strokeWidth = 2.dp.toPx()
|
val strokeWidth = 2.dp.toPx()
|
||||||
drawLine(
|
drawLine(
|
||||||
color = color,
|
color = color,
|
||||||
@ -399,7 +550,7 @@ private fun BackArrowIcon(modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PlusIcon(color: Color, modifier: Modifier = Modifier) {
|
private fun PlusIcon(color: Color, modifier: Modifier = Modifier) {
|
||||||
Canvas(modifier = modifier.size(24.dp)) {
|
androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) {
|
||||||
val strokeWidth = 2.dp.toPx()
|
val strokeWidth = 2.dp.toPx()
|
||||||
val cx = size.width / 2
|
val cx = size.width / 2
|
||||||
val cy = size.height / 2
|
val cy = size.height / 2
|
||||||
@ -423,7 +574,7 @@ private fun PlusIcon(color: Color, modifier: Modifier = Modifier) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CalendarIcon(color: Color, modifier: Modifier = Modifier) {
|
private fun CalendarIcon(color: Color, modifier: Modifier = Modifier) {
|
||||||
Canvas(modifier = modifier.size(24.dp)) {
|
androidx.compose.foundation.Canvas(modifier = modifier.size(24.dp)) {
|
||||||
val strokeWidth = 1.5f.dp.toPx()
|
val strokeWidth = 1.5f.dp.toPx()
|
||||||
val pad = 3.dp.toPx()
|
val pad = 3.dp.toPx()
|
||||||
val topY = pad + 4.dp.toPx()
|
val topY = pad + 4.dp.toPx()
|
||||||
@ -431,13 +582,11 @@ private fun CalendarIcon(color: Color, modifier: Modifier = Modifier) {
|
|||||||
val leftX = pad
|
val leftX = pad
|
||||||
val rightX = size.width - pad
|
val rightX = size.width - pad
|
||||||
|
|
||||||
// 外框
|
|
||||||
drawLine(color, Offset(leftX, topY), Offset(rightX, topY), strokeWidth)
|
drawLine(color, Offset(leftX, topY), Offset(rightX, topY), strokeWidth)
|
||||||
drawLine(color, Offset(leftX, topY), Offset(leftX, bottomY), strokeWidth)
|
drawLine(color, Offset(leftX, topY), Offset(leftX, bottomY), strokeWidth)
|
||||||
drawLine(color, Offset(rightX, topY), Offset(rightX, bottomY), strokeWidth)
|
drawLine(color, Offset(rightX, topY), Offset(rightX, bottomY), strokeWidth)
|
||||||
drawLine(color, Offset(leftX, bottomY), Offset(rightX, bottomY), strokeWidth)
|
drawLine(color, Offset(leftX, bottomY), Offset(rightX, bottomY), strokeWidth)
|
||||||
|
|
||||||
// 顶部挂环
|
|
||||||
val h1 = size.width * 0.3f
|
val h1 = size.width * 0.3f
|
||||||
val h2 = size.width * 0.7f
|
val h2 = size.width * 0.7f
|
||||||
drawLine(color, Offset(h1, pad), Offset(h1, topY), strokeWidth)
|
drawLine(color, Offset(h1, pad), Offset(h1, topY), strokeWidth)
|
||||||
@ -445,6 +594,35 @@ private fun CalendarIcon(color: Color, modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ArrowRightIcon(color: Color, modifier: Modifier = Modifier) {
|
||||||
|
androidx.compose.foundation.Canvas(modifier = modifier) {
|
||||||
|
val strokeWidth = 2.dp.toPx()
|
||||||
|
val y = size.height / 2
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(0f, y),
|
||||||
|
end = Offset(size.width * 0.65f, y),
|
||||||
|
strokeWidth = strokeWidth,
|
||||||
|
cap = StrokeCap.Round
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(size.width * 0.4f, y - size.height * 0.3f),
|
||||||
|
end = Offset(size.width * 0.65f, y),
|
||||||
|
strokeWidth = strokeWidth,
|
||||||
|
cap = StrokeCap.Round
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(size.width * 0.4f, y + size.height * 0.3f),
|
||||||
|
end = Offset(size.width * 0.65f, y),
|
||||||
|
strokeWidth = strokeWidth,
|
||||||
|
cap = StrokeCap.Round
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Helpers
|
// region Helpers
|
||||||
@ -455,4 +633,23 @@ private fun LocalDate.toEpochMillis(): Long =
|
|||||||
private fun Long.toLocalDate(): LocalDate =
|
private fun Long.toLocalDate(): LocalDate =
|
||||||
Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.UTC).date
|
Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.UTC).date
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") // monthNumber/dayOfMonth 无替代 API,kotlinx-datetime 尚未提供新接口
|
||||||
|
private fun LocalDate.formatChinese(): String =
|
||||||
|
"${year}年${monthNumber}月${dayOfMonth}日"
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION", "Unused") // monthNumber/dayOfMonth 无替代 API
|
||||||
|
private fun LocalDate.formatShortChinese(): String =
|
||||||
|
"${monthNumber}月${dayOfMonth}日"
|
||||||
|
|
||||||
|
private fun LocalDate.dayOfWeekChinese(): String = when (dayOfWeek.ordinal) {
|
||||||
|
0 -> "周一"
|
||||||
|
1 -> "周二"
|
||||||
|
2 -> "周三"
|
||||||
|
3 -> "周四"
|
||||||
|
4 -> "周五"
|
||||||
|
5 -> "周六"
|
||||||
|
6 -> "周日"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
Loading…
x
Reference in New Issue
Block a user