From 55d50c3e1d38375d2771488a27399cd17965ba3f Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:24:38 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20clampExpiryDay?= =?UTF-8?q?s=20=E7=BA=AF=E5=87=BD=E6=95=B0=E9=92=B3=E5=88=B6=E4=BF=9D?= =?UTF-8?q?=E8=B4=A8=E6=9C=9F=E5=A4=A9=E6=95=B0=E4=B8=BA=E9=9D=9E=E8=B4=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plus/rua/project/ui/DateCheckerScreen.kt | 11 +++++++ .../project/ui/DateCheckerScreenLogicTest.kt | 29 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 core/src/test/kotlin/plus/rua/project/ui/DateCheckerScreenLogicTest.kt diff --git a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt index e876a68..2dd3811 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt @@ -745,6 +745,17 @@ private fun ArrowRightIcon(color: Color, modifier: Modifier = Modifier) { // region Helpers +/** + * 将保质期天数钳制到合法范围 [0, +∞)。 + * + * 天数语义上不能为负(到期日不应早于生产日期)。 + * 无论来自天数输入框还是日期选择器,写入 [ExpiryRow.days] 前都应经过此函数。 + * + * @param days 原始天数 + * @return 钳制后的天数,最小为 0 + */ +fun clampExpiryDays(days: Int): Int = days.coerceAtLeast(0) + private fun LocalDate.toEpochMillis(): Long = this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds() diff --git a/core/src/test/kotlin/plus/rua/project/ui/DateCheckerScreenLogicTest.kt b/core/src/test/kotlin/plus/rua/project/ui/DateCheckerScreenLogicTest.kt new file mode 100644 index 0000000..660491c --- /dev/null +++ b/core/src/test/kotlin/plus/rua/project/ui/DateCheckerScreenLogicTest.kt @@ -0,0 +1,29 @@ +package plus.rua.project.ui + +import kotlin.test.Test +import kotlin.test.assertEquals + +class DateCheckerScreenLogicTest { + + // ---- clampExpiryDays ---- + + @Test + fun clampExpiryDays_positiveValue_unchanged() { + assertEquals(30, clampExpiryDays(30)) + } + + @Test + fun clampExpiryDays_zero_unchanged() { + assertEquals(0, clampExpiryDays(0)) + } + + @Test + fun clampExpiryDays_negativeValue_clampedToZero() { + assertEquals(0, clampExpiryDays(-1)) + } + + @Test + fun clampExpiryDays_largeNegative_clampedToZero() { + assertEquals(0, clampExpiryDays(-365)) + } +} From 2085c22987a686b9f3cf433710051d1e19b885f8 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:27:35 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E6=97=A5=E6=9C=9F=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=99=A8=E5=86=99=E5=85=A5=E4=BF=9D=E8=B4=A8=E6=9C=9F=E5=A4=A9?= =?UTF-8?q?=E6=95=B0=E5=89=8D=E9=92=B3=E5=88=B6=E4=B8=BA=E9=9D=9E=E8=B4=9F?= =?UTF-8?q?,=E9=98=B2=E6=AD=A2=E8=B4=9F=E6=95=B0=E8=90=BD=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt index 2dd3811..b129abf 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt @@ -365,7 +365,8 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { } }, onExpiryDateChange = { newDate -> - val newDays = productionDate.daysUntil(newDate) + val rawDays = productionDate.daysUntil(newDate) + val newDays = clampExpiryDays(rawDays) rows = rows.map { if (it.id == row.id) it.copy(days = newDays) else it } @@ -463,7 +464,8 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { when (val target = datePickerTarget) { is DatePickerTarget.Production -> productionDate = selected is DatePickerTarget.Row -> { - val newDays = productionDate.daysUntil(selected) + val rawDays = productionDate.daysUntil(selected) + val newDays = clampExpiryDays(rawDays) rows = rows.map { if (it.id == target.rowId) it.copy(days = newDays) else it } From 1438b405a96ce2611342545b7ca6e32d68724213 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:30:43 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E6=97=A5=E6=9C=9F=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=99=A8=20Row=20=E6=97=A5=E6=9C=9F=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=E7=A6=81=E9=80=89=E6=97=A9=E4=BA=8E=E7=94=9F=E4=BA=A7?= =?UTF-8?q?=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plus/rua/project/ui/DateCheckerScreen.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt index b129abf..601b6bb 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold +import androidx.compose.material3.SelectableDates import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text @@ -453,7 +454,23 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { null -> productionDate.toEpochMillis() } - val datePickerState = rememberDatePickerState(initialSelectedDateMillis = initialMillis) + val productionMillis = productionDate.toEpochMillis() + // Row 日期选择器禁选早于生产日期(到期日不应在生产之前); + // Production 选择器本身不受限制。当前 BOM 无 SelectableDates.AllDates, + // 用空实现 object 等价于默认全允许(default 方法均返回 true)。 + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = initialMillis, + selectableDates = when (datePickerTarget) { + is DatePickerTarget.Row -> object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean = + utcTimeMillis >= productionMillis + + override fun isSelectableYear(year: Int): Boolean = + year >= productionDate.year + } + else -> object : SelectableDates {} + } + ) DatePickerDialog( onDismissRequest = { showDatePicker = false }, From 7628f299c20052aa2fe7fee8e495fa20206d131e Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 15 Jun 2026 11:40:26 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E5=8A=A0=E8=BD=BD=E4=BF=9D=E8=B4=A8?= =?UTF-8?q?=E6=9C=9F=E5=88=97=E8=A1=A8=E6=97=B6=E9=92=B3=E5=88=B6=E6=97=A7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE,=E6=B8=85=E7=90=86=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E7=9A=84=E8=B4=9F=E6=95=B0=E5=A4=A9?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt index 601b6bb..c57c137 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt @@ -143,8 +143,9 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { var productionDate by remember { mutableStateOf(saved?.first ?: today) } var rows by remember { mutableStateOf( + // clampExpiryDays 兜底:清理本修复前可能持久化的负数旧数据 (saved?.second ?: defaultRows).mapIndexed { index, days -> - ExpiryRow(index, days) + ExpiryRow(index, clampExpiryDays(days)) } ) }