From 9584d46247e1f18fdcaaf5bb0729d71a944afcef Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 10:54:07 +0800 Subject: [PATCH 01/38] Change collapse animation to staggered row slide-up with fade-out Rows now keep full height and slide upward sequentially instead of compressing. The anchor row (selected) moves to y=0 and stays fixed; other rows exit top-to-bottom with staggered timing. --- .../plus/rua/project/ui/CalendarMonthPage.kt | 105 +++++++++--------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index 51da8c4..eb35c29 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -2,7 +2,6 @@ package plus.rua.project.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset @@ -23,10 +22,10 @@ import kotlinx.datetime.number import kotlinx.datetime.plus /** - * 月度日历网格页面,支持折叠动画。 + * 月度日历网格页面,支持逐行向上滑出的折叠动画。 * - * 折叠时非选中行高度按 (1-p) 缩放,选中行保持原始高度, - * 所有行通过手动 y-offset 定位,形成向选中行收缩的视觉效果。 + * 折叠时锚定行(包含选中日期)平滑移动到顶部固定,其余行从上到下依次向上滑出并淡出。 + * 下方行从锚定行背后经过(z-index 遮挡),所有行高度不变,仅做 y 平移。 * * @param year 年份 * @param month 月份(1-12) @@ -58,14 +57,19 @@ fun CalendarMonthPage( val density = LocalDensity.current val weeks = days.chunked(7) - val selectedWeekIndex = remember(weeks, selectedDate) { + val anchorIndex = remember(weeks, selectedDate) { weeks.indexOfFirst { week -> week.any { it.date == selectedDate } } } - - val hasSelectedWeek = selectedWeekIndex >= 0 + val hasAnchor = anchorIndex >= 0 val h = rowHeightPx.toFloat() - // 使用与 CalendarMonthView 一致的 effectiveWeeks 计算高度,避免滑动中高度不匹配 + // Stagger 参数:每行的动画延迟和持续时间 + val totalNonAnchor = if (hasAnchor) weeks.size - 1 else weeks.size + val staggerGap = if (totalNonAnchor > 1) 0.5f / totalNonAnchor else 0f + val rowAnimDuration = if (totalNonAnchor > 1) { + (1f - (totalNonAnchor - 1) * staggerGap).coerceAtLeast(0.1f) + } else 1f + val totalHeightDp = if (rowHeightPx > 0) { val totalPx = h * (1 + (effectiveWeeks - 1) * (1f - collapseProgress)) with(density) { totalPx.toDp() } @@ -80,49 +84,46 @@ fun CalendarMonthPage( ) ) { weeks.forEachIndexed { weekIndex, week -> - val isSelected = hasSelectedWeek && weekIndex == selectedWeekIndex - val isAbove = hasSelectedWeek && weekIndex < selectedWeekIndex - val isBelow = hasSelectedWeek && weekIndex > selectedWeekIndex + val isAnchor = hasAnchor && weekIndex == anchorIndex - val rowScale = when { - isAbove || isBelow -> 1f - collapseProgress - else -> 1f + // 退出顺序:从上到下视觉顺序,锚定行跳过 + val exitOrder = when { + !hasAnchor -> weekIndex + weekIndex < anchorIndex -> weekIndex + weekIndex == anchorIndex -> -1 + else -> weekIndex - 1 } - val rowHeightDp = if (rowHeightPx > 0 && rowScale > 0.01f) { - with(density) { (h * rowScale).toDp() } - } else if (rowHeightPx <= 0) { - null - } else { - 0.dp + // 每行的局部进度(staggered) + val localProgress = when { + collapseProgress <= 0f -> 0f + isAnchor -> collapseProgress + exitOrder < 0 -> 0f + totalNonAnchor <= 1 -> collapseProgress + else -> ((collapseProgress - exitOrder * staggerGap) / rowAnimDuration).coerceIn(0f, 1f) } - val yOffsetDp = if (rowHeightPx > 0 && hasSelectedWeek) { - val yPx = when { - isAbove -> weekIndex * h * (1f - collapseProgress) - isSelected -> selectedWeekIndex * h * (1f - collapseProgress) - isBelow -> selectedWeekIndex * h * (1f - collapseProgress) + h + (weekIndex - selectedWeekIndex - 1) * h * (1f - collapseProgress) - else -> weekIndex * h + // Y 偏移 + val yOffsetDp = if (rowHeightPx > 0) { + val yPx = if (isAnchor) { + anchorIndex * h * (1f - localProgress) + } else { + val originalY = weekIndex * h + originalY - localProgress * (originalY + h) } with(density) { yPx.toDp() } - } else if (rowHeightPx > 0) { - val yPx = weekIndex * h - with(density) { yPx.toDp() } - } else { - 0.dp - } + } else 0.dp - val shouldShow = rowHeightDp == null || rowHeightDp > 0.dp + // 淡出 + val rowAlpha = if (isAnchor) 1f else (1f - localProgress).coerceIn(0f, 1f) - val skipDayCells = (isAbove || isBelow) && rowScale < 0.1f && collapseProgress > 0.9f - - if (shouldShow) { + if (rowAlpha > 0.01f) { Row( modifier = Modifier .fillMaxWidth() - .zIndex(if (isSelected) 1f else 0f) + .zIndex(if (isAnchor) 1f else 0f) .then( - if (rowHeightDp != null) Modifier.height(rowHeightDp) + if (rowHeightPx > 0) Modifier.height(with(density) { h.toDp() }) else Modifier ) .offset(y = yOffsetDp) @@ -137,25 +138,19 @@ fun CalendarMonthPage( ) .padding(vertical = ROW_PADDING_DP.dp) .then( - if (isAbove || isBelow) Modifier.graphicsLayer { - alpha = 1f - collapseProgress - } + if (rowAlpha < 1f) Modifier.graphicsLayer { alpha = rowAlpha } else Modifier ) ) { - if (skipDayCells) { - Spacer(Modifier.weight(1f)) - } else { - week.forEach { dayData -> - DayCell( - date = dayData.date, - isCurrentMonth = dayData.isCurrentMonth, - isSelected = dayData.date == selectedDate, - isToday = dayData.date == today, - onClick = { onDateClick(dayData.date) }, - modifier = Modifier.weight(1f) - ) - } + week.forEach { dayData -> + DayCell( + date = dayData.date, + isCurrentMonth = dayData.isCurrentMonth, + isSelected = dayData.date == selectedDate, + isToday = dayData.date == today, + onClick = { onDateClick(dayData.date) }, + modifier = Modifier.weight(1f) + ) } } } @@ -185,4 +180,4 @@ private fun generateMonthDays(year: Int, month: Int): List { isCurrentMonth = date.month.number == month && date.year == year ) } -} \ No newline at end of file +} From 666dae3b9d7c477fecc6fda18eeaffffc0721f9a Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 11:02:59 +0800 Subject: [PATCH 02/38] Rewrite collapse animation as two-phase whole-block slide-up Phase 1: all rows slide up together until selected row reaches y=0. Phase 2: rows below selected row slide up as a group. No per-row height scaling, only y-offset translation + fade. --- .../plus/rua/project/ui/CalendarMonthPage.kt | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index eb35c29..9f6fb93 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -22,10 +22,10 @@ import kotlinx.datetime.number import kotlinx.datetime.plus /** - * 月度日历网格页面,支持逐行向上滑出的折叠动画。 + * 月度日历网格页面,支持两阶段折叠动画。 * - * 折叠时锚定行(包含选中日期)平滑移动到顶部固定,其余行从上到下依次向上滑出并淡出。 - * 下方行从锚定行背后经过(z-index 遮挡),所有行高度不变,仅做 y 平移。 + * Phase 1:所有行整体上移,直到选中行到达顶部 (y=0),上方行被裁剪并淡出。 + * Phase 2:选中行固定不动,下方行整体上移并淡出。 * * @param year 年份 * @param month 月份(1-12) @@ -63,19 +63,27 @@ fun CalendarMonthPage( val hasAnchor = anchorIndex >= 0 val h = rowHeightPx.toFloat() - // Stagger 参数:每行的动画延迟和持续时间 - val totalNonAnchor = if (hasAnchor) weeks.size - 1 else weeks.size - val staggerGap = if (totalNonAnchor > 1) 0.5f / totalNonAnchor else 0f - val rowAnimDuration = if (totalNonAnchor > 1) { - (1f - (totalNonAnchor - 1) * staggerGap).coerceAtLeast(0.1f) - } else 1f + // Phase 1 结束点:选中行到顶部所需的比例 + val phase1End = if (hasAnchor && anchorIndex > 0 && weeks.size > 1) { + anchorIndex.toFloat() / (weeks.size - 1) + } else 0f + + val phase1 = if (phase1End > 0f) { + (collapseProgress / phase1End).coerceIn(0f, 1f) + } else if (collapseProgress > 0f) 1f else 0f + + val phase2 = if (phase1End < 1f && collapseProgress > phase1End) { + ((collapseProgress - phase1End) / (1f - phase1End)).coerceIn(0f, 1f) + } else 0f + + val belowRowsHeight = if (hasAnchor) { + (weeks.size - 1 - anchorIndex) * h + } else 0f val totalHeightDp = if (rowHeightPx > 0) { val totalPx = h * (1 + (effectiveWeeks - 1) * (1f - collapseProgress)) with(density) { totalPx.toDp() } - } else { - null - } + } else null Box( modifier = modifier.clipToBounds().then( @@ -85,37 +93,27 @@ fun CalendarMonthPage( ) { weeks.forEachIndexed { weekIndex, week -> val isAnchor = hasAnchor && weekIndex == anchorIndex + val isAbove = hasAnchor && weekIndex < anchorIndex + val isBelow = hasAnchor && weekIndex > anchorIndex - // 退出顺序:从上到下视觉顺序,锚定行跳过 - val exitOrder = when { - !hasAnchor -> weekIndex - weekIndex < anchorIndex -> weekIndex - weekIndex == anchorIndex -> -1 - else -> weekIndex - 1 - } - - // 每行的局部进度(staggered) - val localProgress = when { - collapseProgress <= 0f -> 0f - isAnchor -> collapseProgress - exitOrder < 0 -> 0f - totalNonAnchor <= 1 -> collapseProgress - else -> ((collapseProgress - exitOrder * staggerGap) / rowAnimDuration).coerceIn(0f, 1f) - } - - // Y 偏移 val yOffsetDp = if (rowHeightPx > 0) { - val yPx = if (isAnchor) { - anchorIndex * h * (1f - localProgress) - } else { - val originalY = weekIndex * h - originalY - localProgress * (originalY + h) + val yPx = when { + !hasAnchor -> weekIndex * h - collapseProgress * weeks.size * h + isAnchor -> anchorIndex * h * (1f - phase1) + isAbove -> weekIndex * h - phase1 * anchorIndex * h + isBelow -> weekIndex * h - phase1 * anchorIndex * h - phase2 * belowRowsHeight + else -> weekIndex * h } with(density) { yPx.toDp() } } else 0.dp - // 淡出 - val rowAlpha = if (isAnchor) 1f else (1f - localProgress).coerceIn(0f, 1f) + val rowAlpha = when { + !hasAnchor -> (1f - collapseProgress).coerceIn(0f, 1f) + isAnchor -> 1f + isAbove -> (1f - phase1).coerceIn(0f, 1f) + isBelow -> (1f - phase2).coerceIn(0f, 1f) + else -> 1f + } if (rowAlpha > 0.01f) { Row( From 9a0c8771a14db5d0c7517c00a23fe77f4f5fbc80 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 11:10:30 +0800 Subject: [PATCH 03/38] Add row background color to prevent text bleed during collapse animation --- .../commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index 9f6fb93..ac09135 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -1,5 +1,6 @@ package plus.rua.project.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -15,6 +16,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.compose.material3.MaterialTheme import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate import kotlinx.datetime.minus @@ -124,6 +126,7 @@ fun CalendarMonthPage( if (rowHeightPx > 0) Modifier.height(with(density) { h.toDp() }) else Modifier ) + .background(MaterialTheme.colorScheme.surface) .offset(y = yOffsetDp) .then( if (weekIndex == 0 && rowHeightPx == 0) { From 7c6c32486d47ce2a6a889c292c2c0e0dd4b55999 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 11:18:09 +0800 Subject: [PATCH 04/38] Only add background to anchor row during collapse --- .../kotlin/plus/rua/project/ui/CalendarMonthPage.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index ac09135..f5089f2 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -126,7 +126,10 @@ fun CalendarMonthPage( if (rowHeightPx > 0) Modifier.height(with(density) { h.toDp() }) else Modifier ) - .background(MaterialTheme.colorScheme.surface) + .then( + if (isAnchor) Modifier.background(MaterialTheme.colorScheme.surface) + else Modifier + ) .offset(y = yOffsetDp) .then( if (weekIndex == 0 && rowHeightPx == 0) { From 9544cb7526bc07ac75c1751c0ade49fec9991fb9 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 11:24:34 +0800 Subject: [PATCH 05/38] Only show anchor row background during collapse, not on initial render --- .../commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index f5089f2..4c85da7 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -127,7 +127,7 @@ fun CalendarMonthPage( else Modifier ) .then( - if (isAnchor) Modifier.background(MaterialTheme.colorScheme.surface) + if (isAnchor && collapseProgress > 0f) Modifier.background(MaterialTheme.colorScheme.surface) else Modifier ) .offset(y = yOffsetDp) From 6a0ceaf8d39b9a69f5a9bafcfc239095abd32cf8 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 11:29:13 +0800 Subject: [PATCH 06/38] Show anchor row background only after it reaches y=0 --- .../commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index 4c85da7..bcbbc5a 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -127,7 +127,7 @@ fun CalendarMonthPage( else Modifier ) .then( - if (isAnchor && collapseProgress > 0f) Modifier.background(MaterialTheme.colorScheme.surface) + if (isAnchor && phase1 >= 1f) Modifier.background(MaterialTheme.colorScheme.surface) else Modifier ) .offset(y = yOffsetDp) From e403c683f6a6d3cb7d64ae7682e1cec192d7ebed Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 12:24:12 +0800 Subject: [PATCH 07/38] Sync CalendarPager to selectedDate month when expanding from week view --- .../plus/rua/project/ui/CalendarMonthView.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index c24ae65..d805129 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -60,6 +61,22 @@ fun CalendarMonthView( val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE }) + // 展开时同步 CalendarPager 页面到 selectedDate 所在月份 + LaunchedEffect(viewModel.isCollapsed) { + if (!viewModel.isCollapsed) { + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + viewModel.selectedDate.year, + viewModel.selectedDate.month.number, + today.year, + today.month.number + ) + if (targetPage != pagerState.currentPage) { + pagerState.scrollToPage(targetPage) + } + } + } + val collapseProgress = viewModel.collapseProgress val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx val rowPaddingPx = with(density) { ROW_PADDING_DP.dp.toPx() }.toInt() From 104c5e5baa70e5c530801f8e5b037b6b16c11d2b Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 13:10:03 +0800 Subject: [PATCH 08/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=E6=80=81=E8=B7=A8=E6=9C=88=E5=91=A8=E9=80=89=E4=B8=AD=E4=B8=8A?= =?UTF-8?q?=E4=B8=AA=E6=9C=88=E6=97=A5=E6=9C=9F=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 跨月周(如5月第一周周一是4月27日)改为选中下个月的1号, 避免月份标题和展开内容不一致。 --- .../kotlin/plus/rua/project/ui/CalendarMonthView.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index d805129..433b56f 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -179,9 +179,16 @@ fun CalendarMonthView( today = today, onDateClick = { date -> viewModel.selectDate(date) }, onWeekChanged = { weekMonday -> - // 优先选中当周内的今天,否则选中该周周一 val weekSunday = weekMonday.plus(DatePeriod(days = 6)) - val date = if (today in weekMonday..weekSunday) today else weekMonday + val date = when { + today in weekMonday..weekSunday -> today + weekMonday.month != weekSunday.month -> { + // 跨月周:选中下个月的1号 + @Suppress("DEPRECATION") // monthNumber 无替代 API + LocalDate(weekSunday.year, weekSunday.month.number, 1) + } + else -> weekMonday + } viewModel.selectDate(date) }, modifier = pagerModifier From 857cf88cb0ec1ad78fe2847fded4676b445e7c9f Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 13:22:47 +0800 Subject: [PATCH 09/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B7=A8=E6=9C=88?= =?UTF-8?q?=E5=91=A8=E9=80=89=E4=B8=AD=E9=80=BB=E8=BE=91=EF=BC=9A=E6=A0=B9?= =?UTF-8?q?=E6=8D=AE=E6=BB=91=E5=8A=A8=E6=96=B9=E5=90=91=E5=86=B3=E5=AE=9A?= =?UTF-8?q?=E9=80=89=E4=B8=AD=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后退到跨月周(如从5月滑到4月27-5月3):选中较晚月份1号,留在当月。 前进到跨月周(如从4月滑到4月27-5月3):选中该周周一,留在上个月。 --- .../kotlin/plus/rua/project/ui/CalendarMonthView.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index 433b56f..c697854 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -183,9 +183,14 @@ fun CalendarMonthView( val date = when { today in weekMonday..weekSunday -> today weekMonday.month != weekSunday.month -> { - // 跨月周:选中下个月的1号 - @Suppress("DEPRECATION") // monthNumber 无替代 API - LocalDate(weekSunday.year, weekSunday.month.number, 1) + if (weekMonday < viewModel.selectedDate) { + // 后退到跨月周(如从5月回到4月27-5月3):选较晚月份1号 + @Suppress("DEPRECATION") // monthNumber 无替代 API + LocalDate(weekSunday.year, weekSunday.month.number, 1) + } else { + // 前进到跨月周(如从4月前进到4月27-5月3):选该周周一 + weekMonday + } } else -> weekMonday } From 09e13e335c7f33cd3e74fbc382efed65069e82fa Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 13:28:29 +0800 Subject: [PATCH 10/38] =?UTF-8?q?Revert=20=E8=B7=A8=E6=9C=88=E5=91=A8?= =?UTF-8?q?=E9=80=89=E4=B8=AD=E9=80=BB=E8=BE=91=E7=9A=84=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=EF=BC=8C=E9=87=8D=E6=96=B0=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plus/rua/project/ui/CalendarMonthView.kt | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index c697854..d805129 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -179,21 +179,9 @@ fun CalendarMonthView( today = today, onDateClick = { date -> viewModel.selectDate(date) }, onWeekChanged = { weekMonday -> + // 优先选中当周内的今天,否则选中该周周一 val weekSunday = weekMonday.plus(DatePeriod(days = 6)) - val date = when { - today in weekMonday..weekSunday -> today - weekMonday.month != weekSunday.month -> { - if (weekMonday < viewModel.selectedDate) { - // 后退到跨月周(如从5月回到4月27-5月3):选较晚月份1号 - @Suppress("DEPRECATION") // monthNumber 无替代 API - LocalDate(weekSunday.year, weekSunday.month.number, 1) - } else { - // 前进到跨月周(如从4月前进到4月27-5月3):选该周周一 - weekMonday - } - } - else -> weekMonday - } + val date = if (today in weekMonday..weekSunday) today else weekMonday viewModel.selectDate(date) }, modifier = pagerModifier From fcad070800f2435d170ecd5377dab9ba97d97ec3 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 13:38:34 +0800 Subject: [PATCH 11/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=E6=80=81=E8=B7=A8=E6=9C=88=E5=91=A8=E9=80=89=E4=B8=AD=E5=92=8C?= =?UTF-8?q?=E5=B1=95=E5=BC=80=E9=97=AA=E8=B7=B3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 展开同步逻辑移至 CalendarPager(LaunchedEffect(Unit)),减少闪帧 2. 跨月周根据滑动方向选中日期: - 后退到跨月周:选较晚月份1号,留在当月 - 前进到跨月周:选该周周一,留在上个月 --- .../plus/rua/project/ui/CalendarMonthView.kt | 33 ++++++++----------- .../plus/rua/project/ui/CalendarPager.kt | 11 +++++++ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index d805129..58feb7d 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -61,22 +60,6 @@ fun CalendarMonthView( val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE }) - // 展开时同步 CalendarPager 页面到 selectedDate 所在月份 - LaunchedEffect(viewModel.isCollapsed) { - if (!viewModel.isCollapsed) { - @Suppress("DEPRECATION") // monthNumber 无替代 API - val targetPage = yearMonthToPage( - viewModel.selectedDate.year, - viewModel.selectedDate.month.number, - today.year, - today.month.number - ) - if (targetPage != pagerState.currentPage) { - pagerState.scrollToPage(targetPage) - } - } - } - val collapseProgress = viewModel.collapseProgress val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx val rowPaddingPx = with(density) { ROW_PADDING_DP.dp.toPx() }.toInt() @@ -179,9 +162,21 @@ fun CalendarMonthView( today = today, onDateClick = { date -> viewModel.selectDate(date) }, onWeekChanged = { weekMonday -> - // 优先选中当周内的今天,否则选中该周周一 val weekSunday = weekMonday.plus(DatePeriod(days = 6)) - val date = if (today in weekMonday..weekSunday) today else weekMonday + val date = when { + today in weekMonday..weekSunday -> today + weekMonday.month != weekSunday.month -> { + if (weekMonday < viewModel.selectedDate) { + // 后退到跨月周(如从5月回到4月27-5月3):选较晚月份1号,留在当月 + @Suppress("DEPRECATION") // monthNumber 无替代 API + LocalDate(weekSunday.year, weekSunday.month.number, 1) + } else { + // 前进到跨月周(如从4月前进到4月27-5月3):选该周周一,留在上个月 + weekMonday + } + } + else -> weekMonday + } viewModel.selectDate(date) }, modifier = pagerModifier diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt index 1d29e21..6d96511 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -52,6 +52,17 @@ fun CalendarPager( val initialMonth = remember { today.month.number } val coroutineScope = rememberCoroutineScope() + // 展开后同步页面到 selectedDate 所在月份(修复折叠态切月后展开闪跳) + LaunchedEffect(Unit) { + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + selectedDate.year, selectedDate.month.number, initialYear, initialMonth + ) + if (targetPage != pagerState.currentPage) { + pagerState.scrollToPage(targetPage) + } + } + // 跳过初始发射,保留首次渲染时的"今天"选中状态 LaunchedEffect(pagerState) { snapshotFlow { pagerState.settledPage }.drop(1).collect { page -> From b65856a4ae256518bcbd5aa932bd7cb64bcbe35f Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 14:54:35 +0800 Subject: [PATCH 12/38] =?UTF-8?q?=E7=82=B9=E5=87=BB=E6=9C=88=E4=BB=BD?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E8=B7=B3=E8=BD=AC=E5=88=B0=E4=BB=8A=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/plus/rua/project/ui/CalendarMonthView.kt | 12 ++++++++++++ .../kotlin/plus/rua/project/ui/MonthHeader.kt | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index 58feb7d..ce3c726 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -27,6 +27,7 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.number import kotlinx.datetime.plus import kotlinx.datetime.todayIn +import kotlinx.coroutines.launch import plus.rua.project.CalendarViewModel import kotlin.math.abs import kotlin.time.Clock @@ -144,6 +145,17 @@ fun CalendarMonthView( year = currentYear, month = currentMonth, weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate), + onClick = { + viewModel.selectDate(today) + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + today.year, today.month.number, + today.year, today.month.number + ) + if (targetPage != pagerState.currentPage) { + coroutineScope.launch { pagerState.animateScrollToPage(targetPage) } + } + }, modifier = Modifier.onSizeChanged { size -> monthHeaderHeightPx = size.height } diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt index e8d3ca1..c8b42b7 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -32,12 +33,14 @@ fun MonthHeader( year: Int, month: Int, weekNumber: Int, + onClick: (() -> Unit)? = null, modifier: Modifier = Modifier ) { Row( modifier = modifier .fillMaxWidth() - .padding(vertical = 14.dp, horizontal = 12.dp), + .padding(vertical = 14.dp, horizontal = 12.dp) + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), verticalAlignment = Alignment.CenterVertically ) { AnimatedContent( From c86cdd61b8ea14929788c6cc06ee61c3ba474284 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 14:57:24 +0800 Subject: [PATCH 13/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20BottomCard=20?= =?UTF-8?q?=E6=B7=B1=E8=89=B2=E6=A8=A1=E5=BC=8F=E9=80=82=E9=85=8D=E5=92=8C?= =?UTF-8?q?=20DayCell=20=E6=97=A0=E9=9A=9C=E7=A2=8D=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt | 3 +-- shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt index 56a7ebf..d4b9ccf 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable 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.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp @@ -87,7 +86,7 @@ fun BottomCard( .align(Alignment.TopCenter) .padding(top = 8.dp, bottom = 8.dp) .clip(RoundedCornerShape(2.dp)) - .background(Color.Gray.copy(alpha = 0.4f)) + .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)) .fillMaxWidth(0.15f) .height(4.dp) ) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 03bb0c5..72f9f46 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.style.TextAlign @@ -106,6 +108,10 @@ fun DayCell( Box( modifier = modifier .aspectRatio(1f) + .semantics { + @Suppress("DEPRECATION") + contentDescription = "${date.year}年${date.monthNumber}月${date.day}日" + } .clip(CircleShape) .drawBehind { if (revealProgress > 0f) { From dfda6fa5a9d17bc68c55281d953572f3ec8dd96c Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 15:15:27 +0800 Subject: [PATCH 14/38] =?UTF-8?q?DayCell=20=E5=A2=9E=E5=8A=A0=E5=86=9C?= =?UTF-8?q?=E5=8E=86=E6=97=A5=E6=9C=9F=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 tyme4kt 库在日期数字下方显示农历日名,初一显示月名。 --- gradle/libs.versions.toml | 2 + shared/build.gradle.kts | 1 + .../kotlin/plus/rua/project/ui/DayCell.kt | 55 +++++++++++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb02672..559e70d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ junit = "4.13.2" kotlin = "2.3.21" material3 = "1.10.0-alpha05" kotlinx-datetime = "0.8.0" +tyme4kt = "1.4.4" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -28,6 +29,7 @@ compose-components-resources = { module = "org.jetbrains.compose.components:comp compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version = "1.11.0" } +tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 34aaf6b..433742c 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -45,6 +45,7 @@ kotlin { implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.kotlinx.datetime) + implementation(libs.tyme4kt) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 72f9f46..ff0f69c 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -7,12 +7,15 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,7 +26,10 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tyme.solar.SolarDay import kotlinx.datetime.LocalDate enum class DayCellState { @@ -105,6 +111,30 @@ fun DayCell( val todayBorderColor = MaterialTheme.colorScheme.primary + val lunarText = remember(date) { + val lunarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day).getLunarDay() + val name = lunarDay.getName() + if (name == "初一") { + val lunarMonth = lunarDay.getLunarMonth() + "${lunarMonth.getName()}月" + } else { + name + } + } + + val lunarColor by transition.animateColor( + transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, + label = "lunarColor" + ) { state -> + when (state) { + DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) + DayCellState.TODAY -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) + DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f) + DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + } + } + Box( modifier = modifier .aspectRatio(1f) @@ -134,11 +164,24 @@ fun DayCell( .clickable(onClick = onClick), contentAlignment = Alignment.Center ) { - Text( - text = date.day.toString(), - textAlign = TextAlign.Center, - color = contentColor, - style = MaterialTheme.typography.bodyMedium - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = date.day.toString(), + textAlign = TextAlign.Center, + color = contentColor, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = lunarText, + textAlign = TextAlign.Center, + color = lunarColor, + fontSize = 7.sp, + maxLines = 1, + overflow = TextOverflow.Clip, + lineHeight = 9.sp + ) + } } } From 055738220c91d545d2a7b232501871d0f6ceb518 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 15:22:18 +0800 Subject: [PATCH 15/38] Add holiday, festival and solar term annotations to DayCell Replace plain lunar text with priority-based annotations: legal holidays > lunar festivals > solar terms > solar festivals > lunar day. Holiday/festival text uses error color to stand out from regular lunar text. --- .../kotlin/plus/rua/project/ui/DayCell.kt | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index ff0f69c..c56f9c8 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -111,27 +111,68 @@ fun DayCell( val todayBorderColor = MaterialTheme.colorScheme.primary - val lunarText = remember(date) { - val lunarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day).getLunarDay() + data class DayAnnotation(val text: String, val isHighlight: Boolean) + + val annotation = remember(date) { + val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) + val lunarDay = solarDay.getLunarDay() + + // 法定假日优先 + val legalHoliday = solarDay.getLegalHoliday() + if (legalHoliday != null) { + val suffix = if (legalHoliday.isWork()) "班" else "休" + return@remember DayAnnotation("${legalHoliday.getName()}$suffix", true) + } + + // 农历传统节日 + val lunarFestival = lunarDay.getFestival() + if (lunarFestival != null) { + return@remember DayAnnotation(lunarFestival.getName(), true) + } + + // 节气(当天才显示) + val termDay = solarDay.getTermDay() + if (termDay.getDayIndex() == 0) { + return@remember DayAnnotation(termDay.getSolarTerm().getName(), true) + } + + // 公历节日 + val solarFestival = solarDay.getFestival() + if (solarFestival != null) { + return@remember DayAnnotation(solarFestival.getName(), true) + } + + // 默认:农历日期 val name = lunarDay.getName() - if (name == "初一") { + val text = if (name == "初一") { val lunarMonth = lunarDay.getLunarMonth() "${lunarMonth.getName()}月" } else { name } + DayAnnotation(text, false) } val lunarColor by transition.animateColor( transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, label = "lunarColor" ) { state -> - when (state) { - DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) - DayCellState.TODAY -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) - DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f) - DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + if (annotation.isHighlight) { + when (state) { + DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.85f) + DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.85f) + DayCellState.TODAY -> MaterialTheme.colorScheme.primary + DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.error.copy(alpha = 0.35f) + DayCellState.NORMAL -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) + } + } else { + when (state) { + DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) + DayCellState.TODAY -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) + DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f) + DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + } } } @@ -174,7 +215,7 @@ fun DayCell( style = MaterialTheme.typography.bodyMedium ) Text( - text = lunarText, + text = annotation.text, textAlign = TextAlign.Center, color = lunarColor, fontSize = 7.sp, From 418b97baed43e4db0b96282f610350fd09da757f Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 15:32:26 +0800 Subject: [PATCH 16/38] Fix week number baseline alignment and collapsed state today jump - MonthHeader: align week number text baseline with month text (Bottom) - WeekPager: scroll to selectedDate's week when it changes externally, fixing the case where clicking "back to today" in collapsed state didn't navigate the week pager to the current week --- .../kotlin/plus/rua/project/ui/MonthHeader.kt | 2 +- .../commonMain/kotlin/plus/rua/project/ui/WeekPager.kt | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt index c8b42b7..2b8f151 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt @@ -41,7 +41,7 @@ fun MonthHeader( .fillMaxWidth() .padding(vertical = 14.dp, horizontal = 12.dp) .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.Bottom ) { AnimatedContent( targetState = Pair(year, month), diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt index 6173edb..97f7a13 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.drop import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil import kotlinx.datetime.plus import kotlin.math.abs @@ -42,6 +43,15 @@ fun WeekPager( pageCount = { Int.MAX_VALUE } ) + // selectedDate 外部变更(如点击回到今天)时,滚动到对应周 + LaunchedEffect(selectedDate) { + val targetMonday = selectedDate.toWeekMonday() + val targetPage = START_PAGE + (initialWeekMonday.daysUntil(targetMonday) / 7) + if (pagerState.currentPage != targetPage) { + pagerState.animateScrollToPage(targetPage) + } + } + LaunchedEffect(pagerState) { snapshotFlow { pagerState.settledPage }.drop(1).collect { page -> val weekMonday = pageToWeekMonday(page, initialWeekMonday) From 888c4d03d39d5a5ba38f0c4743047df22509c29a Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 15:39:38 +0800 Subject: [PATCH 17/38] Use baseline alignment for month text and week number --- .../commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt index 2b8f151..a7f2e0e 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt @@ -41,7 +41,7 @@ fun MonthHeader( .fillMaxWidth() .padding(vertical = 14.dp, horizontal = 12.dp) .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), - verticalAlignment = Alignment.Bottom + verticalAlignment = Alignment.CenterVertically ) { AnimatedContent( targetState = Pair(year, month), @@ -53,7 +53,8 @@ fun MonthHeader( slideInVertically(tween(250)) { it } + fadeIn(tween(250)) togetherWith slideOutVertically(tween(250)) { -it } + fadeOut(tween(250)) } - } + }, + modifier = Modifier.alignByBaseline() ) { (y, m) -> Text( text = "${y}年${m}月", @@ -71,7 +72,8 @@ fun MonthHeader( slideInVertically(tween(250)) { it } + fadeIn(tween(250)) togetherWith slideOutVertically(tween(250)) { -it } + fadeOut(tween(250)) } - } + }, + modifier = Modifier.alignByBaseline() ) { week -> Text( text = "第${week}周", From b0b97650ec1fcd8abcb3fb9c0d1f21bdb41e9444 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 15:45:03 +0800 Subject: [PATCH 18/38] Fix week number alignment: use Bottom + padding instead of alignByBaseline alignByBaseline caused layout jumps during AnimatedContent transitions. Switch to Bottom alignment with 2dp bottom padding on week number for stable visual alignment with month text baseline. --- .../commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt index a7f2e0e..e2e9e31 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt @@ -41,7 +41,7 @@ fun MonthHeader( .fillMaxWidth() .padding(vertical = 14.dp, horizontal = 12.dp) .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.Bottom ) { AnimatedContent( targetState = Pair(year, month), @@ -53,8 +53,7 @@ fun MonthHeader( slideInVertically(tween(250)) { it } + fadeIn(tween(250)) togetherWith slideOutVertically(tween(250)) { -it } + fadeOut(tween(250)) } - }, - modifier = Modifier.alignByBaseline() + } ) { (y, m) -> Text( text = "${y}年${m}月", @@ -73,7 +72,7 @@ fun MonthHeader( slideOutVertically(tween(250)) { -it } + fadeOut(tween(250)) } }, - modifier = Modifier.alignByBaseline() + modifier = Modifier.padding(bottom = 2.dp) ) { week -> Text( text = "第${week}周", From 16b73c4373f55d5e495be03b8d511ec4198a90ec Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 15:58:12 +0800 Subject: [PATCH 19/38] Fix flash when expanding after navigating months in collapsed state Sync CalendarPager's pagerState to selectedDate in CalendarMonthView via LaunchedEffect(selectedDate), so the page is already correct when CalendarPager re-enters composition during expand. Remove the now- redundant LaunchedEffect(Unit) sync in CalendarPager. --- .../plus/rua/project/ui/CalendarMonthView.kt | 14 ++++++++++++++ .../kotlin/plus/rua/project/ui/CalendarPager.kt | 11 ----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index ce3c726..5e66966 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -61,6 +62,19 @@ fun CalendarMonthView( val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE }) + // 折叠态 WeekPager 切月时,持续同步 CalendarPager 的 pagerState, + // 避免展开时 CalendarPager 首帧显示旧月份导致闪白 + LaunchedEffect(viewModel.selectedDate) { + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + viewModel.selectedDate.year, viewModel.selectedDate.month.number, + today.year, today.month.number + ) + if (targetPage != pagerState.currentPage) { + pagerState.scrollToPage(targetPage) + } + } + val collapseProgress = viewModel.collapseProgress val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx val rowPaddingPx = with(density) { ROW_PADDING_DP.dp.toPx() }.toInt() diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt index 6d96511..1d29e21 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -52,17 +52,6 @@ fun CalendarPager( val initialMonth = remember { today.month.number } val coroutineScope = rememberCoroutineScope() - // 展开后同步页面到 selectedDate 所在月份(修复折叠态切月后展开闪跳) - LaunchedEffect(Unit) { - @Suppress("DEPRECATION") // monthNumber 无替代 API - val targetPage = yearMonthToPage( - selectedDate.year, selectedDate.month.number, initialYear, initialMonth - ) - if (targetPage != pagerState.currentPage) { - pagerState.scrollToPage(targetPage) - } - } - // 跳过初始发射,保留首次渲染时的"今天"选中状态 LaunchedEffect(pagerState) { snapshotFlow { pagerState.settledPage }.drop(1).collect { page -> From 995693cb5d0ef9ee970b98008ea03ce506b8d6ee Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 16:29:17 +0800 Subject: [PATCH 20/38] Add year view with Hero Zoom transition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CalendarViewModel: add isYearView, yearViewProgress, yearViewYear state with toggleYearView(), selectMonthFromYearView(), year navigation methods - YearGridView: new 4x3 month grid with year navigation header - MonthHeader: onClick now toggles year view, added "今天" button - CalendarMonthView: overlay year view with graphicsLayer anchor-based scale transition, hide BottomCard during year view --- .../plus/rua/project/ui/YearGridView.kt | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt new file mode 100644 index 0000000..720cf0c --- /dev/null +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt @@ -0,0 +1,154 @@ +package plus.rua.project.ui + +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +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 + +/** + * 年度网格视图,显示 4×3 月份网格,支持年份切换。 + * + * @param year 显示的年份 + * @param selectedMonth 当前选中月份(1-12) + * @param onMonthClick 月份点击回调 + * @param onYearChange 年份切换回调 + * @param modifier 外部布局修饰符 + */ +@Composable +fun YearGridView( + year: Int, + selectedMonth: Int, + onMonthClick: (Int) -> Unit, + onYearChange: (Int) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 年份导航行 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "‹", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clip(CircleShape) + .clickable { onYearChange(year - 1) } + .padding(horizontal = 16.dp, vertical = 8.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "${year}年", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "›", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clip(CircleShape) + .clickable { onYearChange(year + 1) } + .padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + // 4×3 月份网格 + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.SpaceEvenly + ) { + (0 until 4).forEach { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + (0 until 3).forEach { col -> + val month = row * 3 + col + 1 + MonthCell( + month = month, + isSelected = month == selectedMonth, + onClick = { onMonthClick(month) }, + modifier = Modifier.weight(1f) + ) + } + } + } + } + } +} + +/** + * 年视图中的月份单元格。 + */ +@Composable +private fun MonthCell( + month: Int, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val contentColor = if (isSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + } + val bgColor = MaterialTheme.colorScheme.primary + + Box( + modifier = modifier + .aspectRatio(1f) + .padding(6.dp) + .clip(CircleShape) + .drawBehind { + if (isSelected) { + drawCircle( + color = bgColor, + radius = size.minDimension / 2f, + center = Offset(size.width / 2f, size.height / 2f) + ) + } + } + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + Text( + text = "${month}月", + color = contentColor, + fontSize = 16.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, + textAlign = TextAlign.Center + ) + } +} From 731a1bb6a1ef8047048f4562058b485fe7a542db Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 16:29:37 +0800 Subject: [PATCH 21/38] Wire year view into CalendarMonthView, MonthHeader and ViewModel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CalendarViewModel: year view state and animation methods - CalendarMonthView: graphicsLayer zoom overlay, BottomCard hiding - MonthHeader: toggle year view on click, "今天" button --- .../plus/rua/project/CalendarViewModel.kt | 57 ++++++++ .../plus/rua/project/ui/CalendarMonthView.kt | 125 ++++++++++++------ .../kotlin/plus/rua/project/ui/MonthHeader.kt | 24 +++- 3 files changed, 164 insertions(+), 42 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index d328ac2..e55ac3b 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -1,7 +1,9 @@ package plus.rua.project import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -61,6 +63,16 @@ class CalendarViewModel( val currentYear: Int get() = selectedDate.year + var isYearView by mutableStateOf(false) + private set + + private val _yearViewAnimatable = Animatable(0f) + val yearViewProgress: Float get() = _yearViewAnimatable.value + + @Suppress("DEPRECATION") // monthNumber 无替代 API + var yearViewYear by mutableStateOf(today.year) + private set + /** * 选中指定日期。 * @@ -70,6 +82,51 @@ class CalendarViewModel( selectedDate = date } + /** + * 切换年视图。仅在展开态可用。 + */ + fun toggleYearView() { + if (isCollapsed) return + coroutineScope.launch { + if (isYearView) { + _yearViewAnimatable.animateTo( + 0f, tween(400, easing = FastOutSlowInEasing) + ) + isYearView = false + } else { + isYearView = true + _yearViewAnimatable.snapTo(0f) + _yearViewAnimatable.animateTo( + 1f, tween(400, easing = FastOutSlowInEasing) + ) + } + } + } + + /** + * 从年视图选择月份后返回月视图。 + */ + @Suppress("DEPRECATION") // monthNumber 无替代 API + fun selectMonthFromYearView(month: Int) { + val date = if (yearViewYear == today.year && today.month.number == month) today + else LocalDate(yearViewYear, month, 1) + selectedDate = date + coroutineScope.launch { + _yearViewAnimatable.animateTo( + 0f, tween(400, easing = FastOutSlowInEasing) + ) + isYearView = false + } + } + + fun incrementYear() { + yearViewYear++ + } + + fun decrementYear() { + yearViewYear-- + } + /** * 展开状态下拖拽折叠,delta 正值推动 progress 向 1(折叠方向)。 * diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index 5e66966..a6350fc 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -19,6 +19,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp @@ -34,10 +36,10 @@ import kotlin.math.abs import kotlin.time.Clock /** - * 日历主界面,包含月/周视图切换和折叠动画。 + * 日历主界面,包含月/周视图切换、折叠动画和年视图缩放转场。 * * 折叠时日历从月视图收缩为周视图(1行),BottomCard 同步上移填充空间。 - * 支持动态行数(4/5/6行),滑动切换月份时 BottomCard 跟手移动。 + * 点击月份标题切换年视图,以当前月为锚点缩放转场。 * * @param modifier 外部布局修饰符 */ @@ -59,11 +61,11 @@ fun CalendarMonthView( var rowHeightPx by remember { mutableIntStateOf(0) } var screenWidthPx by remember { mutableIntStateOf(0) } var screenHeightPx by remember { mutableIntStateOf(0) } + var calendarContentHeightPx by remember { mutableIntStateOf(0) } val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE }) - // 折叠态 WeekPager 切月时,持续同步 CalendarPager 的 pagerState, - // 避免展开时 CalendarPager 首帧显示旧月份导致闪白 + // 折叠态 WeekPager 切月时,持续同步 CalendarPager 的 pagerState LaunchedEffect(viewModel.selectedDate) { @Suppress("DEPRECATION") // monthNumber 无替代 API val targetPage = yearMonthToPage( @@ -76,6 +78,7 @@ fun CalendarMonthView( } val collapseProgress = viewModel.collapseProgress + val yearProgress = viewModel.yearViewProgress val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx val rowPaddingPx = with(density) { ROW_PADDING_DP.dp.toPx() }.toInt() val cardGapPx = with(density) { @@ -86,8 +89,6 @@ fun CalendarMonthView( ).dp.toPx() }.toInt() - // 翻页时在相邻月份行数之间插值,使 BottomCard 高度平滑过渡 - // abs(fraction) > 阈值时启用插值,避免静止时的浮点抖动 val interpolatedWeeks by remember { derivedStateOf { val fraction = pagerState.currentPageOffsetFraction @@ -103,9 +104,6 @@ fun CalendarMonthView( } } - // 预估行高:DayCell aspectRatio=1,宽度 = (screenWidth - horizontalPadding) / 7 - // 加上 Row 的 vertical padding (6dp × 2) - // 用于 rowHeightPx 尚未测量时的 fallback,避免首次布局高度为 0 val estimatedRowHeightPx = if (screenWidthPx > 0) { val cellWidth = (screenWidthPx - with(density) { (HORIZONTAL_PADDING_DP * 2).dp.toPx() }) / 7 @@ -114,14 +112,8 @@ fun CalendarMonthView( } else 0 val effectiveRowHeightPx = if (rowHeightPx > 0) rowHeightPx else estimatedRowHeightPx - val effectiveWeeks = interpolatedWeeks - // 折叠时网格高度公式(与 CalendarMonthPage 一致): - // collapseProgress=0 展开时 gridH = rowH × weeks;collapseProgress=1 折叠时 gridH = rowH × 1 - // 中间态:gridH = rowH × (1 + (weeks-1) × (1-collapseProgress)) - // 直接计算而非 derivedStateOf:effectiveRowHeightPx 依赖 rowHeightPx state, - // derivedStateOf 无法追踪非 State 局部变量,rowHeightPx 从 0 变为测量值时 gridHeightPx 不会更新 val gridHeightPx = if (effectiveRowHeightPx > 0) { val rowH = effectiveRowHeightPx.toFloat() if (collapseProgress > OFFSET_FRACTION_THRESHOLD) { @@ -131,12 +123,10 @@ fun CalendarMonthView( } } else 0 - // BottomCard 高度 = 屏幕剩余空间(屏幕高度 - 日历区域高度) val calendarAreaHeightPx = headerHeightPx + gridHeightPx + rowPaddingPx + cardGapPx val cardHeightPx = if (screenHeightPx > 0 && calendarAreaHeightPx > 0) screenHeightPx - calendarAreaHeightPx else 0 - // 行高已知时约束 pager 高度防止内容溢出;否则让 pager 自由扩展以触发首次行高测量 val pagerModifier = if (rowHeightPx > 0 && gridHeightPx > 0) { Modifier .height(with(density) { gridHeightPx.toDp() }) @@ -145,6 +135,18 @@ fun CalendarMonthView( Modifier } + // 年视图锚点缩放:当前月在 4×3 网格中的归一化位置 + val anchorPivotX = ((currentMonth - 1) % 3 + 0.5f) / 3f + val anchorPivotY = ((currentMonth - 1) / 3 + 0.5f) / 4f + + // 月视图层缩放:从 1f 缩小到 ~0.3f(年网格单格 vs 月视图大小比) + val monthScale = 1f - yearProgress * 0.7f + val monthAlpha = (1f - yearProgress * 1.4f).coerceIn(0f, 1f) + + // 年视图层缩放:从 ~3.3f 放大到 1f + val yearScale = lerp(3.3f, 1f, yearProgress) + val yearAlpha = ((yearProgress - 0.2f) / 0.8f).coerceIn(0f, 1f) + Box( modifier = modifier .fillMaxSize() @@ -154,12 +156,25 @@ fun CalendarMonthView( screenHeightPx = size.height } ) { - Column(modifier = Modifier.padding(horizontal = HORIZONTAL_PADDING_DP.dp)) { + // 月视图层 + Column( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = monthScale + scaleY = monthScale + alpha = monthAlpha + transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) + } + .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + ) { MonthHeader( year = currentYear, month = currentMonth, weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate), - onClick = { + showToday = viewModel.selectedDate != today, + onToggleYearView = { viewModel.toggleYearView() }, + onToday = { viewModel.selectDate(today) @Suppress("DEPRECATION") // monthNumber 无替代 API val targetPage = yearMonthToPage( @@ -180,8 +195,6 @@ fun CalendarMonthView( weekdayHeaderHeightPx = size.height } ) - // 完全折叠且无动画时切换到 WeekPager(单行高效渲染), - // 否则使用 CalendarPager(含折叠动画和下拉恢复过程) if (viewModel.isCollapsed && viewModel.collapseProgress >= 1f) { WeekPager( selectedDate = viewModel.selectedDate, @@ -193,11 +206,9 @@ fun CalendarMonthView( today in weekMonday..weekSunday -> today weekMonday.month != weekSunday.month -> { if (weekMonday < viewModel.selectedDate) { - // 后退到跨月周(如从5月回到4月27-5月3):选较晚月份1号,留在当月 @Suppress("DEPRECATION") // monthNumber 无替代 API LocalDate(weekSunday.year, weekSunday.month.number, 1) } else { - // 前进到跨月周(如从4月前进到4月27-5月3):选该周周一,留在上个月 weekMonday } } @@ -213,8 +224,7 @@ fun CalendarMonthView( today = today, onDateClick = { date -> viewModel.selectDate(date) }, onMonthChanged = { year, month -> - // 优先选中当月内的今天,否则选中该月1号 - @Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口 + @Suppress("DEPRECATION") // monthNumber 无替代 API val date = if (year == today.year && today.month.number == month) today else LocalDate(year, month, 1) viewModel.selectDate(date) @@ -231,23 +241,58 @@ fun CalendarMonthView( } } - // 拖拽范围 = 折叠时日历实际高度变化量 (weeks-1)×rowHeight,使手指移动与视觉变化 1:1 对应 - val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() } - val dragRangePx = if (effectiveRowHeightPx > 0) { - maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx) - } else { - dragRangeMinPx + // 年视图层 + if (viewModel.isYearView || yearProgress > 0.01f) { + YearGridView( + year = viewModel.yearViewYear, + selectedMonth = if (viewModel.yearViewYear == currentYear) currentMonth else 0, + onMonthClick = { month -> + viewModel.selectMonthFromYearView(month) + // 同步 CalendarPager 到目标月份 + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + viewModel.yearViewYear, month, + today.year, today.month.number + ) + if (targetPage != pagerState.currentPage) { + coroutineScope.launch { pagerState.scrollToPage(targetPage) } + } + }, + onYearChange = { newYear -> + if (newYear > viewModel.yearViewYear) viewModel.incrementYear() + else viewModel.decrementYear() + }, + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = yearScale + scaleY = yearScale + alpha = yearAlpha + transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) + } + .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + ) } - if (cardHeightPx > 0) { - BottomCard( - viewModel = viewModel, - dragRangePx = dragRangePx, - modifier = Modifier - .fillMaxWidth() - .height(with(density) { cardHeightPx.toDp() }) - .align(Alignment.BottomCenter) - ) + // BottomCard:年视图时隐藏 + if (yearProgress < 0.01f) { + val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() } + val dragRangePx = if (effectiveRowHeightPx > 0) { + maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx) + } else { + dragRangeMinPx + } + + if (cardHeightPx > 0) { + BottomCard( + viewModel = viewModel, + dragRangePx = dragRangePx, + modifier = Modifier + .fillMaxWidth() + .height(with(density) { cardHeightPx.toDp() }) + .align(Alignment.BottomCenter) + ) + } } } } diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt index e2e9e31..66342b6 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/MonthHeader.kt @@ -13,12 +13,15 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp /** * 月份标题栏,显示"年月"文字和 ISO 周号。 @@ -26,6 +29,9 @@ import androidx.compose.ui.unit.dp * @param year 年份 * @param month 月份(1-12) * @param weekNumber 当前 ISO 周号 + * @param showToday 是否显示「今天」按钮(当 selectedDate ≠ today 时) + * @param onToggleYearView 点击标题切换年视图 + * @param onToday 点击「今天」按钮跳转今天 * @param modifier 外部布局修饰符 */ @Composable @@ -33,14 +39,16 @@ fun MonthHeader( year: Int, month: Int, weekNumber: Int, - onClick: (() -> Unit)? = null, + showToday: Boolean, + onToggleYearView: () -> Unit, + onToday: (() -> Unit)? = null, modifier: Modifier = Modifier ) { Row( modifier = modifier .fillMaxWidth() .padding(vertical = 14.dp, horizontal = 12.dp) - .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), + .clickable(onClick = onToggleYearView), verticalAlignment = Alignment.Bottom ) { AnimatedContent( @@ -79,5 +87,17 @@ fun MonthHeader( style = MaterialTheme.typography.bodySmall ) } + Spacer(modifier = Modifier.weight(1f)) + if (showToday && onToday != null) { + Text( + text = "今天", + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp, + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable(onClick = onToday) + .padding(horizontal = 10.dp, vertical = 4.dp) + ) + } } } From c996d026cc7c68f6e1f88c81cb4662f035601463 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 16:35:56 +0800 Subject: [PATCH 22/38] Replace year view month cells with mini calendar grids Each month in the 4x3 year grid now shows a compact calendar with day numbers, matching the iOS Calendar year view style. Today is highlighted with a filled circle. Selected month title uses primary color. --- .../plus/rua/project/ui/CalendarMonthView.kt | 1 + .../plus/rua/project/ui/YearGridView.kt | 140 +++++++++++++----- 2 files changed, 107 insertions(+), 34 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index a6350fc..21a0885 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -246,6 +246,7 @@ fun CalendarMonthView( YearGridView( year = viewModel.yearViewYear, selectedMonth = if (viewModel.yearViewYear == currentYear) currentMonth else 0, + today = today, onMonthClick = { month -> viewModel.selectMonthFromYearView(month) // 同步 CalendarPager 到目标月份 diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt index 720cf0c..abb18bb 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt @@ -5,8 +5,6 @@ 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.padding @@ -14,6 +12,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,9 +22,18 @@ 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 kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.number +import kotlinx.datetime.plus +import kotlinx.datetime.todayIn /** - * 年度网格视图,显示 4×3 月份网格,支持年份切换。 + * 年度网格视图,显示 4×3 精简月历网格,支持年份切换。 + * + * 每格显示一个精简版月历(月份标题 + 日期数字网格), + * 选中月份高亮,点击进入该月。 * * @param year 显示的年份 * @param selectedMonth 当前选中月份(1-12) @@ -37,6 +45,7 @@ import androidx.compose.ui.unit.sp fun YearGridView( year: Int, selectedMonth: Int, + today: LocalDate, onMonthClick: (Int) -> Unit, onYearChange: (Int) -> Unit, modifier: Modifier = Modifier @@ -62,13 +71,13 @@ fun YearGridView( .clickable { onYearChange(year - 1) } .padding(horizontal = 16.dp, vertical = 8.dp) ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${year}年", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold - ) - Spacer(modifier = Modifier.weight(1f)) + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) { + Text( + text = "${year}年", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } Text( text = "›", fontSize = 24.sp, @@ -81,12 +90,12 @@ fun YearGridView( ) } - // 4×3 月份网格 + // 4×3 月历网格 Column( modifier = Modifier .fillMaxWidth() .weight(1f) - .padding(horizontal = 16.dp), + .padding(horizontal = 8.dp), verticalArrangement = Arrangement.SpaceEvenly ) { (0 until 4).forEach { row -> @@ -96,9 +105,11 @@ fun YearGridView( ) { (0 until 3).forEach { col -> val month = row * 3 + col + 1 - MonthCell( + MiniMonth( + year = year, month = month, isSelected = month == selectedMonth, + today = today, onClick = { onMonthClick(month) }, modifier = Modifier.weight(1f) ) @@ -110,45 +121,106 @@ fun YearGridView( } /** - * 年视图中的月份单元格。 + * 精简版月历:月份标题 + 日期数字网格。 */ @Composable -private fun MonthCell( +private fun MiniMonth( + year: Int, month: Int, isSelected: Boolean, + today: LocalDate, onClick: () -> Unit, modifier: Modifier = Modifier ) { - val contentColor = if (isSelected) { - MaterialTheme.colorScheme.onPrimary + val days = remember(year, month) { generateMiniMonthDays(year, month) } + val titleColor = if (isSelected) { + MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.onSurface } - val bgColor = MaterialTheme.colorScheme.primary + val dayColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + val otherMonthColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) + val todayBgColor = MaterialTheme.colorScheme.primary - Box( + Column( modifier = modifier - .aspectRatio(1f) - .padding(6.dp) + .padding(2.dp) .clip(CircleShape) - .drawBehind { - if (isSelected) { - drawCircle( - color = bgColor, - radius = size.minDimension / 2f, - center = Offset(size.width / 2f, size.height / 2f) - ) - } - } - .clickable(onClick = onClick), - contentAlignment = Alignment.Center + .clickable(onClick = onClick) + .padding(vertical = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { + // 月份标题 Text( text = "${month}月", - color = contentColor, - fontSize = 16.sp, + color = titleColor, + fontSize = 10.sp, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, textAlign = TextAlign.Center ) + // 日期网格 + days.chunked(7).forEach { week -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + week.forEach { dayData -> + val isToday = dayData.date == today + val color = when { + !dayData.isCurrentMonth -> otherMonthColor + isToday -> MaterialTheme.colorScheme.primary + else -> dayColor + } + Box( + contentAlignment = Alignment.Center + ) { + if (isToday) { + Box( + modifier = Modifier + .drawBehind { + drawCircle( + color = todayBgColor, + radius = size.minDimension / 2f, + center = Offset(size.width / 2f, size.height / 2f) + ) + } + .clip(CircleShape) + ) + } + Text( + text = if (dayData.isCurrentMonth) dayData.date.day.toString() else "", + color = if (isToday) MaterialTheme.colorScheme.onPrimary else color, + fontSize = 7.sp, + textAlign = TextAlign.Center, + lineHeight = 10.sp + ) + } + } + } + } + } +} + +private data class MiniDayData( + val date: LocalDate, + val isCurrentMonth: Boolean +) + +@Suppress("DEPRECATION") // monthNumber 无替代 API +private fun generateMiniMonthDays(year: Int, month: Int): List { + val firstOfMonth = LocalDate(year, month, 1) + val offset = firstOfMonth.dayOfWeek.ordinal + val startDate = firstOfMonth.minus(DatePeriod(days = offset)) + val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1) + val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).day + val rows = ((offset + daysInMonth - 1) / 7) + 1 + val totalDays = rows * 7 + + return (0 until totalDays).map { i -> + val date = startDate.plus(DatePeriod(days = i)) + MiniDayData( + date = date, + isCurrentMonth = date.month.number == month && date.year == year + ) } } From 502f1efc0a2f1798704c8d01dea6411494974a9e Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 16:39:58 +0800 Subject: [PATCH 23/38] Fix year view showing stale year: reset to selectedDate.year on enter --- .../src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index e55ac3b..27d72a1 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -94,6 +94,7 @@ class CalendarViewModel( ) isYearView = false } else { + yearViewYear = selectedDate.year isYearView = true _yearViewAnimatable.snapTo(0f) _yearViewAnimatable.animateTo( From 8dad07c0a02c0f537f60f3b19986ea1721a08bcf Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 16:43:32 +0800 Subject: [PATCH 24/38] =?UTF-8?q?=E5=B9=B4=E8=A7=86=E5=9B=BE=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B7=A6=E5=8F=B3=E6=BB=91=E5=8A=A8=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=B9=B4=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 HorizontalPager 包裹年视图,支持手势滑动切年。 ‹ › 按钮改为 animateScrollToPage,与滑动行为一致。 --- .../plus/rua/project/CalendarViewModel.kt | 2 +- .../plus/rua/project/ui/CalendarMonthView.kt | 82 ++++++++++++++----- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index 27d72a1..94e057a 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -71,7 +71,7 @@ class CalendarViewModel( @Suppress("DEPRECATION") // monthNumber 无替代 API var yearViewYear by mutableStateOf(today.year) - private set + internal set /** * 选中指定日期。 diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index 21a0885..b2bf002 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerDefaults import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -16,6 +18,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds @@ -65,6 +68,32 @@ fun CalendarMonthView( val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE }) + // 年视图分页器 + val yearPagerState = rememberPagerState( + initialPage = START_PAGE, + pageCount = { Int.MAX_VALUE } + ) + + // 进入年视图时同步 yearPagerState 到当前年 + LaunchedEffect(viewModel.isYearView) { + if (viewModel.isYearView) { + if (yearPagerState.currentPage != START_PAGE) { + yearPagerState.scrollToPage(START_PAGE) + } + } + } + + // 年视图翻页时同步 yearViewYear + LaunchedEffect(yearPagerState) { + snapshotFlow { yearPagerState.settledPage }.collect { page -> + val offset = page - START_PAGE + val targetYear = viewModel.selectedDate.year + offset + if (targetYear != viewModel.yearViewYear) { + viewModel.yearViewYear = targetYear + } + } + } + // 折叠态 WeekPager 切月时,持续同步 CalendarPager 的 pagerState LaunchedEffect(viewModel.selectedDate) { @Suppress("DEPRECATION") // monthNumber 无替代 API @@ -241,28 +270,12 @@ fun CalendarMonthView( } } - // 年视图层 + // 年视图层:HorizontalPager 支持左右滑动切年 if (viewModel.isYearView || yearProgress > 0.01f) { - YearGridView( - year = viewModel.yearViewYear, - selectedMonth = if (viewModel.yearViewYear == currentYear) currentMonth else 0, - today = today, - onMonthClick = { month -> - viewModel.selectMonthFromYearView(month) - // 同步 CalendarPager 到目标月份 - @Suppress("DEPRECATION") // monthNumber 无替代 API - val targetPage = yearMonthToPage( - viewModel.yearViewYear, month, - today.year, today.month.number - ) - if (targetPage != pagerState.currentPage) { - coroutineScope.launch { pagerState.scrollToPage(targetPage) } - } - }, - onYearChange = { newYear -> - if (newYear > viewModel.yearViewYear) viewModel.incrementYear() - else viewModel.decrementYear() - }, + HorizontalPager( + state = yearPagerState, + beyondViewportPageCount = 1, + flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState), modifier = Modifier .fillMaxSize() .graphicsLayer { @@ -272,7 +285,32 @@ fun CalendarMonthView( transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) } .padding(horizontal = HORIZONTAL_PADDING_DP.dp) - ) + ) { page -> + val pageYear = viewModel.selectedDate.year + (page - START_PAGE) + YearGridView( + year = pageYear, + selectedMonth = if (pageYear == currentYear) currentMonth else 0, + today = today, + onMonthClick = { month -> + viewModel.selectMonthFromYearView(month) + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + viewModel.yearViewYear, month, + today.year, today.month.number + ) + if (targetPage != pagerState.currentPage) { + coroutineScope.launch { pagerState.scrollToPage(targetPage) } + } + }, + onYearChange = { newYear -> + val offset = newYear - pageYear + val targetPage = yearPagerState.currentPage + offset + if (targetPage != yearPagerState.currentPage) { + coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) } + } + } + ) + } } // BottomCard:年视图时隐藏 From 142d0c235a45820977cd8298941ba8aa69fff41d Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 16:49:10 +0800 Subject: [PATCH 25/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B9=B4=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E5=B8=83=E5=B1=80=E5=92=8C=E5=8A=A8=E7=94=BB=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 年视图每个小月历添加星期行头部 - 日期列用 weight(1f) 对齐,去掉 CircleShape 裁剪 - 取消前一个动画 Job 防止快速点击时动画丢失 --- .../plus/rua/project/CalendarViewModel.kt | 9 +++- .../plus/rua/project/ui/YearGridView.kt | 44 +++++++++++++------ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index 94e057a..29d3a68 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate @@ -58,6 +59,8 @@ class CalendarViewModel( private val _collapseAnimatable = Animatable(0f) val collapseProgress: Float get() = _collapseAnimatable.value + private var yearViewJob: Job? = null + @Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口 val currentMonth: Int get() = selectedDate.month.number @@ -87,7 +90,8 @@ class CalendarViewModel( */ fun toggleYearView() { if (isCollapsed) return - coroutineScope.launch { + yearViewJob?.cancel() + yearViewJob = coroutineScope.launch { if (isYearView) { _yearViewAnimatable.animateTo( 0f, tween(400, easing = FastOutSlowInEasing) @@ -112,7 +116,8 @@ class CalendarViewModel( val date = if (yearViewYear == today.year && today.month.number == month) today else LocalDate(yearViewYear, month, 1) selectedDate = date - coroutineScope.launch { + yearViewJob?.cancel() + yearViewJob = coroutineScope.launch { _yearViewAnimatable.animateTo( 0f, tween(400, easing = FastOutSlowInEasing) ) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt index abb18bb..4fae310 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/YearGridView.kt @@ -27,16 +27,18 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.minus import kotlinx.datetime.number import kotlinx.datetime.plus -import kotlinx.datetime.todayIn + +private val WEEKDAY_LABELS = listOf("一", "二", "三", "四", "五", "六", "日") /** * 年度网格视图,显示 4×3 精简月历网格,支持年份切换。 * - * 每格显示一个精简版月历(月份标题 + 日期数字网格), + * 每格显示一个精简版月历(月份标题 + 星期行 + 日期数字网格), * 选中月份高亮,点击进入该月。 * * @param year 显示的年份 * @param selectedMonth 当前选中月份(1-12) + * @param today 今天的日期 * @param onMonthClick 月份点击回调 * @param onYearChange 年份切换回调 * @param modifier 外部布局修饰符 @@ -95,7 +97,7 @@ fun YearGridView( modifier = Modifier .fillMaxWidth() .weight(1f) - .padding(horizontal = 8.dp), + .padding(horizontal = 4.dp), verticalArrangement = Arrangement.SpaceEvenly ) { (0 until 4).forEach { row -> @@ -121,7 +123,7 @@ fun YearGridView( } /** - * 精简版月历:月份标题 + 日期数字网格。 + * 精简版月历:月份标题 + 星期行 + 日期数字网格。 */ @Composable private fun MiniMonth( @@ -138,6 +140,7 @@ private fun MiniMonth( } else { MaterialTheme.colorScheme.onSurface } + val weekdayColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) val dayColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) val otherMonthColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) val todayBgColor = MaterialTheme.colorScheme.primary @@ -145,19 +148,33 @@ private fun MiniMonth( Column( modifier = modifier .padding(2.dp) - .clip(CircleShape) .clickable(onClick = onClick) - .padding(vertical = 4.dp), + .padding(vertical = 2.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // 月份标题 Text( text = "${month}月", color = titleColor, - fontSize = 10.sp, + fontSize = 9.sp, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, textAlign = TextAlign.Center ) + // 星期行 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + WEEKDAY_LABELS.forEach { label -> + Text( + text = label, + color = weekdayColor, + fontSize = 6.sp, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f) + ) + } + } // 日期网格 days.chunked(7).forEach { week -> Row( @@ -165,14 +182,15 @@ private fun MiniMonth( horizontalArrangement = Arrangement.SpaceEvenly ) { week.forEach { dayData -> - val isToday = dayData.date == today + val isToday = dayData.date == today && dayData.isCurrentMonth val color = when { !dayData.isCurrentMonth -> otherMonthColor - isToday -> MaterialTheme.colorScheme.primary + isToday -> MaterialTheme.colorScheme.onPrimary else -> dayColor } Box( - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, + modifier = Modifier.weight(1f) ) { if (isToday) { Box( @@ -189,10 +207,10 @@ private fun MiniMonth( } Text( text = if (dayData.isCurrentMonth) dayData.date.day.toString() else "", - color = if (isToday) MaterialTheme.colorScheme.onPrimary else color, - fontSize = 7.sp, + color = color, + fontSize = 6.sp, textAlign = TextAlign.Center, - lineHeight = 10.sp + lineHeight = 9.sp ) } } From 216ebbf990555f8282371199b8d1cc4bb86d9b2e Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 16:59:27 +0800 Subject: [PATCH 26/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=88=87=E6=8D=A2=E5=B9=B4=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E6=97=A0=E5=8A=A8=E7=94=BB=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 始终组合年视图 HorizontalPager 层(通过 graphicsLayer.alpha=0 隐藏), 避免首次进入时 Pager 组合延迟导致动画首帧丢失。 --- .../plus/rua/project/ui/CalendarMonthView.kt | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index b2bf002..b92a46f 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -270,47 +270,45 @@ fun CalendarMonthView( } } - // 年视图层:HorizontalPager 支持左右滑动切年 - if (viewModel.isYearView || yearProgress > 0.01f) { - HorizontalPager( - state = yearPagerState, - beyondViewportPageCount = 1, - flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState), - modifier = Modifier - .fillMaxSize() - .graphicsLayer { - scaleX = yearScale - scaleY = yearScale - alpha = yearAlpha - transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) + // 年视图层:始终组合以避免首次进入时的组合延迟导致动画丢失 + HorizontalPager( + state = yearPagerState, + beyondViewportPageCount = 1, + flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState), + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = yearScale + scaleY = yearScale + alpha = yearAlpha + transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) + } + .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + ) { page -> + val pageYear = viewModel.selectedDate.year + (page - START_PAGE) + YearGridView( + year = pageYear, + selectedMonth = if (pageYear == currentYear) currentMonth else 0, + today = today, + onMonthClick = { month -> + viewModel.selectMonthFromYearView(month) + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + viewModel.yearViewYear, month, + today.year, today.month.number + ) + if (targetPage != pagerState.currentPage) { + coroutineScope.launch { pagerState.scrollToPage(targetPage) } } - .padding(horizontal = HORIZONTAL_PADDING_DP.dp) - ) { page -> - val pageYear = viewModel.selectedDate.year + (page - START_PAGE) - YearGridView( - year = pageYear, - selectedMonth = if (pageYear == currentYear) currentMonth else 0, - today = today, - onMonthClick = { month -> - viewModel.selectMonthFromYearView(month) - @Suppress("DEPRECATION") // monthNumber 无替代 API - val targetPage = yearMonthToPage( - viewModel.yearViewYear, month, - today.year, today.month.number - ) - if (targetPage != pagerState.currentPage) { - coroutineScope.launch { pagerState.scrollToPage(targetPage) } - } - }, - onYearChange = { newYear -> - val offset = newYear - pageYear - val targetPage = yearPagerState.currentPage + offset - if (targetPage != yearPagerState.currentPage) { - coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) } - } + }, + onYearChange = { newYear -> + val offset = newYear - pageYear + val targetPage = yearPagerState.currentPage + offset + if (targetPage != yearPagerState.currentPage) { + coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) } } - ) - } + } + ) } // BottomCard:年视图时隐藏 From c096651e0f78222c6b67fa608e25c6214f6ce50d Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 17:02:45 +0800 Subject: [PATCH 27/38] =?UTF-8?q?Revert=20"=E4=BF=AE=E5=A4=8D=E9=A6=96?= =?UTF-8?q?=E6=AC=A1=E5=90=AF=E5=8A=A8=E5=88=87=E6=8D=A2=E5=B9=B4=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E6=97=A0=E5=8A=A8=E7=94=BB=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 216ebbf990555f8282371199b8d1cc4bb86d9b2e. --- .../plus/rua/project/ui/CalendarMonthView.kt | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index b92a46f..b2bf002 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -270,45 +270,47 @@ fun CalendarMonthView( } } - // 年视图层:始终组合以避免首次进入时的组合延迟导致动画丢失 - HorizontalPager( - state = yearPagerState, - beyondViewportPageCount = 1, - flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState), - modifier = Modifier - .fillMaxSize() - .graphicsLayer { - scaleX = yearScale - scaleY = yearScale - alpha = yearAlpha - transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) - } - .padding(horizontal = HORIZONTAL_PADDING_DP.dp) - ) { page -> - val pageYear = viewModel.selectedDate.year + (page - START_PAGE) - YearGridView( - year = pageYear, - selectedMonth = if (pageYear == currentYear) currentMonth else 0, - today = today, - onMonthClick = { month -> - viewModel.selectMonthFromYearView(month) - @Suppress("DEPRECATION") // monthNumber 无替代 API - val targetPage = yearMonthToPage( - viewModel.yearViewYear, month, - today.year, today.month.number - ) - if (targetPage != pagerState.currentPage) { - coroutineScope.launch { pagerState.scrollToPage(targetPage) } + // 年视图层:HorizontalPager 支持左右滑动切年 + if (viewModel.isYearView || yearProgress > 0.01f) { + HorizontalPager( + state = yearPagerState, + beyondViewportPageCount = 1, + flingBehavior = PagerDefaults.flingBehavior(state = yearPagerState), + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = yearScale + scaleY = yearScale + alpha = yearAlpha + transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) } - }, - onYearChange = { newYear -> - val offset = newYear - pageYear - val targetPage = yearPagerState.currentPage + offset - if (targetPage != yearPagerState.currentPage) { - coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) } + .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + ) { page -> + val pageYear = viewModel.selectedDate.year + (page - START_PAGE) + YearGridView( + year = pageYear, + selectedMonth = if (pageYear == currentYear) currentMonth else 0, + today = today, + onMonthClick = { month -> + viewModel.selectMonthFromYearView(month) + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + viewModel.yearViewYear, month, + today.year, today.month.number + ) + if (targetPage != pagerState.currentPage) { + coroutineScope.launch { pagerState.scrollToPage(targetPage) } + } + }, + onYearChange = { newYear -> + val offset = newYear - pageYear + val targetPage = yearPagerState.currentPage + offset + if (targetPage != yearPagerState.currentPage) { + coroutineScope.launch { yearPagerState.animateScrollToPage(targetPage) } + } } - } - ) + ) + } } // BottomCard:年视图时隐藏 From b730edc1eb15d665d60305b28bb8679b8912dd4c Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 17:27:39 +0800 Subject: [PATCH 28/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=88=87=E6=8D=A2=E5=B9=B4=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E6=97=A0=E5=8A=A8=E7=94=BB=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过 withFrameNanos 在动画前预留一帧,让年视图先完成首次合成与 布局,避免 HorizontalPager + YearGridView 的初次合成开销吃掉动 画时间段。与之前的「常驻合成」方案相比,本次只调时序、不动渲染 结构,因此不会再触发年视图层在隐藏时拦截触摸事件的回归。 --- .../commonMain/kotlin/plus/rua/project/CalendarViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index 29d3a68..a2454f9 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.core.tween import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameNanos import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -101,6 +102,9 @@ class CalendarViewModel( yearViewYear = selectedDate.year isYearView = true _yearViewAnimatable.snapTo(0f) + // 等待一帧让年视图先完成首次合成与布局, + // 避免首次进入年视图时动画时间被合成开销吞掉。 + withFrameNanos { } _yearViewAnimatable.animateTo( 1f, tween(400, easing = FastOutSlowInEasing) ) From 889a54db0e427f4fc741f9ba6d19752796e48b09 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 17:30:51 +0800 Subject: [PATCH 29/38] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=E5=91=A8=E8=A7=86=E5=9B=BE=E8=B7=A8=E6=9C=88=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E6=9C=AA=E7=BD=AE=E7=81=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WeekPager 之前把 isCurrentMonth 硬编码成 true,导致折叠状态下 当前周里属于上/下个月的日期没有变灰,与展开月视图的灰显约定不 一致。改为按 selectedDate 的年月判定(与 MonthHeader 显示的 月份一致),保持折叠前后的视觉一致性。 --- shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt index 97f7a13..45ba90b 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt @@ -78,7 +78,8 @@ fun WeekPager( val date = weekMonday.plus(DatePeriod(days = dayOffset)) DayCell( date = date, - isCurrentMonth = true, + isCurrentMonth = date.month == selectedDate.month + && date.year == selectedDate.year, isSelected = date == selectedDate, isToday = date == today, onClick = { onDateClick(date) }, From c28eb8d0e5481dec1f65645cc759d9a5db02b31a Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 17:34:53 +0800 Subject: [PATCH 30/38] =?UTF-8?q?=E8=8A=82=E6=97=A5=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8F=AA=E5=9C=A8=E5=BD=93=E5=A4=A9=E6=98=BE=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E4=BC=91/=E7=8F=AD=E6=94=B9=E4=B8=BA=E5=8F=B3=E4=B8=8A?= =?UTF-8?q?=E8=A7=92=E8=A7=92=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前法定假期的「春节休」之类文本占据了整段假期,把假期里出现 的节气(雨水、惊蛰等)和其他节日全部挤掉。现在拆成两条线: - 主标注:按 农历节日 → 节气当天 → 公历节日 → 农历日期 的 优先级,仅在节日/节气当天展示节日名。 - 右上角角标:单独读取法定假期标志,调休「休」为 error 色, 调休「班」为 primary 色;非当月时整体降低不透明度。 DayCell 外层多包一层 Box 承载 aspectRatio,原内层保留圆形裁 剪与涟漪;角标放在外层 TopEnd,避免被 CircleShape 裁掉。 --- .../kotlin/plus/rua/project/ui/DayCell.kt | 128 +++++++++++------- 1 file changed, 77 insertions(+), 51 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index c56f9c8..90b6bcf 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -9,7 +9,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -25,6 +27,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -113,18 +116,18 @@ fun DayCell( data class DayAnnotation(val text: String, val isHighlight: Boolean) + val holidayBadge = remember(date) { + @Suppress("DEPRECATION") // monthNumber 无替代 API + val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) + solarDay.getLegalHoliday()?.let { if (it.isWork()) "班" else "休" } + } + val annotation = remember(date) { + @Suppress("DEPRECATION") // monthNumber 无替代 API val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) val lunarDay = solarDay.getLunarDay() - // 法定假日优先 - val legalHoliday = solarDay.getLegalHoliday() - if (legalHoliday != null) { - val suffix = if (legalHoliday.isWork()) "班" else "休" - return@remember DayAnnotation("${legalHoliday.getName()}$suffix", true) - } - - // 农历传统节日 + // 农历传统节日(仅当天) val lunarFestival = lunarDay.getFestival() if (lunarFestival != null) { return@remember DayAnnotation(lunarFestival.getName(), true) @@ -136,7 +139,7 @@ fun DayCell( return@remember DayAnnotation(termDay.getSolarTerm().getName(), true) } - // 公历节日 + // 公历节日(仅当天) val solarFestival = solarDay.getFestival() if (solarFestival != null) { return@remember DayAnnotation(solarFestival.getName(), true) @@ -176,52 +179,75 @@ fun DayCell( } } + val holidayBadgeColor = when (holidayBadge) { + "休" -> MaterialTheme.colorScheme.error + "班" -> MaterialTheme.colorScheme.primary + else -> Color.Transparent + } + val holidayBadgeAlpha = if (isCurrentMonth) 1f else 0.38f + Box( - modifier = modifier - .aspectRatio(1f) - .semantics { - @Suppress("DEPRECATION") - contentDescription = "${date.year}年${date.monthNumber}月${date.day}日" - } - .clip(CircleShape) - .drawBehind { - if (revealProgress > 0f) { - val maxRadius = size.minDimension / 2f - drawCircle( - color = selectedColor, - radius = revealProgress * maxRadius, - center = Offset(size.width / 2f, size.height / 2f) - ) - } - if (borderAlpha > 0f) { - drawCircle( - color = todayBorderColor.copy(alpha = borderAlpha.coerceAtMost(1f)), - radius = size.minDimension / 2f, - center = Offset(size.width / 2f, size.height / 2f), - style = Stroke(width = borderAlpha.coerceAtMost(1.5f) * 1.5.dp.toPx()) - ) - } - } - .clickable(onClick = onClick), - contentAlignment = Alignment.Center + modifier = modifier.aspectRatio(1f) ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier + .fillMaxSize() + .semantics { + @Suppress("DEPRECATION") + contentDescription = "${date.year}年${date.monthNumber}月${date.day}日" + } + .clip(CircleShape) + .drawBehind { + if (revealProgress > 0f) { + val maxRadius = size.minDimension / 2f + drawCircle( + color = selectedColor, + radius = revealProgress * maxRadius, + center = Offset(size.width / 2f, size.height / 2f) + ) + } + if (borderAlpha > 0f) { + drawCircle( + color = todayBorderColor.copy(alpha = borderAlpha.coerceAtMost(1f)), + radius = size.minDimension / 2f, + center = Offset(size.width / 2f, size.height / 2f), + style = Stroke(width = borderAlpha.coerceAtMost(1.5f) * 1.5.dp.toPx()) + ) + } + } + .clickable(onClick = onClick), + contentAlignment = Alignment.Center ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = date.day.toString(), + textAlign = TextAlign.Center, + color = contentColor, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = annotation.text, + textAlign = TextAlign.Center, + color = lunarColor, + fontSize = 7.sp, + maxLines = 1, + overflow = TextOverflow.Clip, + lineHeight = 9.sp + ) + } + } + if (holidayBadge != null) { Text( - text = date.day.toString(), - textAlign = TextAlign.Center, - color = contentColor, - style = MaterialTheme.typography.bodyMedium - ) - Text( - text = annotation.text, - textAlign = TextAlign.Center, - color = lunarColor, - fontSize = 7.sp, - maxLines = 1, - overflow = TextOverflow.Clip, - lineHeight = 9.sp + text = holidayBadge, + color = holidayBadgeColor.copy(alpha = holidayBadgeAlpha), + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + lineHeight = 9.sp, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 2.dp, end = 4.dp) ) } } From aa223db519a1814ee4ef991d08f17cb0c21efc74 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 17:42:21 +0800 Subject: [PATCH 31/38] =?UTF-8?q?=E5=B9=B4=E6=9C=88=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E6=97=B6=E7=AB=8B=E5=8D=B3=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=BA=90=E8=A7=86=E5=9B=BE=EF=BC=8C=E4=BB=85=E5=AF=B9=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E8=A7=86=E5=9B=BE=E6=92=AD=E6=94=BE=E7=BC=A9=E6=94=BE?= =?UTF-8?q?=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前月↔年切换使用交叉淡入:两层同时合成,源视图渐隐、目标视图渐显。 现改为单向过渡:先翻转 isYearView 让源视图立刻从合成中移除, withFrameNanos 等一帧后再启动目标视图的 scale/alpha 动画,避免抖动。 --- .../plus/rua/project/CalendarViewModel.kt | 13 +- .../plus/rua/project/ui/CalendarMonthView.kt | 228 +++++++++--------- 2 files changed, 126 insertions(+), 115 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index a2454f9..b468972 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -88,22 +88,26 @@ class CalendarViewModel( /** * 切换年视图。仅在展开态可用。 + * + * 切换瞬间立即翻转 isYearView,让对应方向的目标视图立刻接管渲染, + * 当前视图被直接移除;动画只作用在目标视图的 scale/alpha 上。 */ fun toggleYearView() { if (isCollapsed) return yearViewJob?.cancel() yearViewJob = coroutineScope.launch { if (isYearView) { + // 年 → 月:先切换状态让月视图开始合成,再等一帧避免首帧抖动 + isYearView = false + withFrameNanos { } _yearViewAnimatable.animateTo( 0f, tween(400, easing = FastOutSlowInEasing) ) - isYearView = false } else { + // 月 → 年:先切换状态让年视图开始合成 yearViewYear = selectedDate.year isYearView = true _yearViewAnimatable.snapTo(0f) - // 等待一帧让年视图先完成首次合成与布局, - // 避免首次进入年视图时动画时间被合成开销吞掉。 withFrameNanos { } _yearViewAnimatable.animateTo( 1f, tween(400, easing = FastOutSlowInEasing) @@ -120,12 +124,13 @@ class CalendarViewModel( val date = if (yearViewYear == today.year && today.month.number == month) today else LocalDate(yearViewYear, month, 1) selectedDate = date + isYearView = false yearViewJob?.cancel() yearViewJob = coroutineScope.launch { + withFrameNanos { } _yearViewAnimatable.animateTo( 0f, tween(400, easing = FastOutSlowInEasing) ) - isYearView = false } } diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index b2bf002..b7c955b 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -168,13 +168,15 @@ fun CalendarMonthView( val anchorPivotX = ((currentMonth - 1) % 3 + 0.5f) / 3f val anchorPivotY = ((currentMonth - 1) / 3 + 0.5f) / 4f - // 月视图层缩放:从 1f 缩小到 ~0.3f(年网格单格 vs 月视图大小比) - val monthScale = 1f - yearProgress * 0.7f - val monthAlpha = (1f - yearProgress * 1.4f).coerceIn(0f, 1f) + // 过渡进度:0=目标视图刚出现,1=目标视图完全到位。 + // 月→年时 yearProgress 从 0→1,年→月时从 1→0,因此用 isYearView 同步翻转方向。 + val transitionProgress = if (viewModel.isYearView) yearProgress else 1f - yearProgress + val targetAlpha = transitionProgress.coerceIn(0f, 1f) - // 年视图层缩放:从 ~3.3f 放大到 1f - val yearScale = lerp(3.3f, 1f, yearProgress) - val yearAlpha = ((yearProgress - 0.2f) / 0.8f).coerceIn(0f, 1f) + // 月视图层缩放:从 0.3f(年网格单格大小)放大到 1f + val monthScale = lerp(0.3f, 1f, transitionProgress) + // 年视图层缩放:从 3.3f(月视图被放大到一格那么大的反向比例)缩小到 1f + val yearScale = lerp(3.3f, 1f, transitionProgress) Box( modifier = modifier @@ -185,93 +187,118 @@ fun CalendarMonthView( screenHeightPx = size.height } ) { - // 月视图层 - Column( - modifier = Modifier - .fillMaxSize() - .graphicsLayer { - scaleX = monthScale - scaleY = monthScale - alpha = monthAlpha - transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) - } - .padding(horizontal = HORIZONTAL_PADDING_DP.dp) - ) { - MonthHeader( - year = currentYear, - month = currentMonth, - weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate), - showToday = viewModel.selectedDate != today, - onToggleYearView = { viewModel.toggleYearView() }, - onToday = { - viewModel.selectDate(today) - @Suppress("DEPRECATION") // monthNumber 无替代 API - val targetPage = yearMonthToPage( - today.year, today.month.number, - today.year, today.month.number - ) - if (targetPage != pagerState.currentPage) { - coroutineScope.launch { pagerState.animateScrollToPage(targetPage) } - } - }, - modifier = Modifier.onSizeChanged { size -> - monthHeaderHeightPx = size.height - } - ) - WeekdayHeader( - modifier = Modifier.fillMaxWidth().padding(bottom = ROW_PADDING_DP.dp) - .onSizeChanged { size -> - weekdayHeaderHeightPx = size.height - } - ) - if (viewModel.isCollapsed && viewModel.collapseProgress >= 1f) { - WeekPager( - selectedDate = viewModel.selectedDate, - today = today, - onDateClick = { date -> viewModel.selectDate(date) }, - onWeekChanged = { weekMonday -> - val weekSunday = weekMonday.plus(DatePeriod(days = 6)) - val date = when { - today in weekMonday..weekSunday -> today - weekMonday.month != weekSunday.month -> { - if (weekMonday < viewModel.selectedDate) { - @Suppress("DEPRECATION") // monthNumber 无替代 API - LocalDate(weekSunday.year, weekSunday.month.number, 1) - } else { - weekMonday - } - } - else -> weekMonday - } - viewModel.selectDate(date) - }, - modifier = pagerModifier - ) + // 月视图层:仅在非年视图时渲染,年视图激活时立即移除。 + if (!viewModel.isYearView) { + val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() } + val dragRangePx = if (effectiveRowHeightPx > 0) { + maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx) } else { - CalendarPager( - selectedDate = viewModel.selectedDate, - today = today, - onDateClick = { date -> viewModel.selectDate(date) }, - onMonthChanged = { year, month -> - @Suppress("DEPRECATION") // monthNumber 无替代 API - val date = if (year == today.year && today.month.number == month) today - else LocalDate(year, month, 1) - viewModel.selectDate(date) - }, - collapseProgress = viewModel.collapseProgress, - rowHeightPx = rowHeightPx, - effectiveWeeks = effectiveWeeks, - onRowHeightMeasured = { h -> - if (h > 0) rowHeightPx = h - }, - pagerState = pagerState, - modifier = pagerModifier - ) + dragRangeMinPx + } + + Box( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + scaleX = monthScale + scaleY = monthScale + alpha = targetAlpha + transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + ) { + MonthHeader( + year = currentYear, + month = currentMonth, + weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate), + showToday = viewModel.selectedDate != today, + onToggleYearView = { viewModel.toggleYearView() }, + onToday = { + viewModel.selectDate(today) + @Suppress("DEPRECATION") // monthNumber 无替代 API + val targetPage = yearMonthToPage( + today.year, today.month.number, + today.year, today.month.number + ) + if (targetPage != pagerState.currentPage) { + coroutineScope.launch { pagerState.animateScrollToPage(targetPage) } + } + }, + modifier = Modifier.onSizeChanged { size -> + monthHeaderHeightPx = size.height + } + ) + WeekdayHeader( + modifier = Modifier.fillMaxWidth().padding(bottom = ROW_PADDING_DP.dp) + .onSizeChanged { size -> + weekdayHeaderHeightPx = size.height + } + ) + if (viewModel.isCollapsed && viewModel.collapseProgress >= 1f) { + WeekPager( + selectedDate = viewModel.selectedDate, + today = today, + onDateClick = { date -> viewModel.selectDate(date) }, + onWeekChanged = { weekMonday -> + val weekSunday = weekMonday.plus(DatePeriod(days = 6)) + val date = when { + today in weekMonday..weekSunday -> today + weekMonday.month != weekSunday.month -> { + if (weekMonday < viewModel.selectedDate) { + @Suppress("DEPRECATION") // monthNumber 无替代 API + LocalDate(weekSunday.year, weekSunday.month.number, 1) + } else { + weekMonday + } + } + else -> weekMonday + } + viewModel.selectDate(date) + }, + modifier = pagerModifier + ) + } else { + CalendarPager( + selectedDate = viewModel.selectedDate, + today = today, + onDateClick = { date -> viewModel.selectDate(date) }, + onMonthChanged = { year, month -> + @Suppress("DEPRECATION") // monthNumber 无替代 API + val date = if (year == today.year && today.month.number == month) today + else LocalDate(year, month, 1) + viewModel.selectDate(date) + }, + collapseProgress = viewModel.collapseProgress, + rowHeightPx = rowHeightPx, + effectiveWeeks = effectiveWeeks, + onRowHeightMeasured = { h -> + if (h > 0) rowHeightPx = h + }, + pagerState = pagerState, + modifier = pagerModifier + ) + } + } + + if (cardHeightPx > 0) { + BottomCard( + viewModel = viewModel, + dragRangePx = dragRangePx, + modifier = Modifier + .fillMaxWidth() + .height(with(density) { cardHeightPx.toDp() }) + .align(Alignment.BottomCenter) + ) + } } } - // 年视图层:HorizontalPager 支持左右滑动切年 - if (viewModel.isYearView || yearProgress > 0.01f) { + // 年视图层:仅在年视图激活时渲染;HorizontalPager 支持左右滑动切年。 + if (viewModel.isYearView) { HorizontalPager( state = yearPagerState, beyondViewportPageCount = 1, @@ -281,7 +308,7 @@ fun CalendarMonthView( .graphicsLayer { scaleX = yearScale scaleY = yearScale - alpha = yearAlpha + alpha = targetAlpha transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) } .padding(horizontal = HORIZONTAL_PADDING_DP.dp) @@ -312,26 +339,5 @@ fun CalendarMonthView( ) } } - - // BottomCard:年视图时隐藏 - if (yearProgress < 0.01f) { - val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() } - val dragRangePx = if (effectiveRowHeightPx > 0) { - maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx) - } else { - dragRangeMinPx - } - - if (cardHeightPx > 0) { - BottomCard( - viewModel = viewModel, - dragRangePx = dragRangePx, - modifier = Modifier - .fillMaxWidth() - .height(with(density) { cardHeightPx.toDp() }) - .align(Alignment.BottomCenter) - ) - } - } } } From 71a3cbc62a4d94b002174bfa72deee3abd79676e Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 18:47:57 +0800 Subject: [PATCH 32/38] =?UTF-8?q?=E9=80=89=E4=B8=AD=E6=80=81:=E4=BB=8A?= =?UTF-8?q?=E5=A4=A9=3D=E5=AE=9E=E5=BF=83=E5=A1=AB=E5=85=85,=E9=9D=9E?= =?UTF-8?q?=E4=BB=8A=E5=A4=A9=3D=E6=8F=8F=E8=BE=B9=E5=9C=86,=E8=A7=92?= =?UTF-8?q?=E6=A0=87=E7=BD=AE=E9=A1=B6=E4=B8=8D=E5=86=8D=E8=A2=AB=E9=81=AE?= =?UTF-8?q?=E6=8C=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前所有选中态都是实心圆,描边圆只用于"今天未选中"。 现改为:今天+选中=primaryContainer 实心填充,普通日期+选中=primary 描边圆, 今天未选中维持细描边。 角标加 zIndex(1f) 提到最上,padding 收紧到 top=1/end=2 紧贴右上角方形空白, 配合描边圆改造,休/班角标在选中态下不再被圆形覆盖。 --- .../kotlin/plus/rua/project/ui/DayCell.kt | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 90b6bcf..628c72a 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import com.tyme.solar.SolarDay import kotlinx.datetime.LocalDate @@ -84,24 +85,35 @@ fun DayCell( ) { state -> when (state) { DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer - DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary + DayCellState.SELECTED -> MaterialTheme.colorScheme.primary DayCellState.TODAY -> MaterialTheme.colorScheme.primary DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface } } - val selectedColor by transition.animateColor( + // 选中今天:实心填充 primaryContainer;其他状态不填充。 + val selectedFillColor by transition.animateColor( transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, - label = "selectedColor" + label = "selectedFillColor" ) { state -> when (state) { DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.primaryContainer - DayCellState.SELECTED -> MaterialTheme.colorScheme.primary else -> Color.Transparent } } + // 选中非今天:绘制描边圆,避免遮挡右上角角标。 + val selectedOutlineAlpha by transition.animateFloat( + transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, + label = "selectedOutlineAlpha" + ) { state -> + when (state) { + DayCellState.SELECTED -> 1f + else -> 0f + } + } + val borderAlpha by transition.animateFloat( transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, label = "borderAlpha" @@ -113,6 +125,7 @@ fun DayCell( } val todayBorderColor = MaterialTheme.colorScheme.primary + val selectedOutlineColor = MaterialTheme.colorScheme.primary data class DayAnnotation(val text: String, val isHighlight: Boolean) @@ -163,7 +176,7 @@ fun DayCell( if (annotation.isHighlight) { when (state) { DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.85f) - DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.85f) + DayCellState.SELECTED -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) DayCellState.TODAY -> MaterialTheme.colorScheme.primary DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.error.copy(alpha = 0.35f) DayCellState.NORMAL -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) @@ -171,7 +184,7 @@ fun DayCell( } else { when (state) { DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) - DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) + DayCellState.SELECTED -> MaterialTheme.colorScheme.primary.copy(alpha = 0.7f) DayCellState.TODAY -> MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f) DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) @@ -198,19 +211,29 @@ fun DayCell( } .clip(CircleShape) .drawBehind { - if (revealProgress > 0f) { - val maxRadius = size.minDimension / 2f + val maxRadius = size.minDimension / 2f + val center = Offset(size.width / 2f, size.height / 2f) + if (revealProgress > 0f && selectedFillColor.alpha > 0f) { drawCircle( - color = selectedColor, + color = selectedFillColor, radius = revealProgress * maxRadius, - center = Offset(size.width / 2f, size.height / 2f) + center = center + ) + } + if (revealProgress > 0f && selectedOutlineAlpha > 0f) { + val strokePx = 1.5.dp.toPx() + drawCircle( + color = selectedOutlineColor.copy(alpha = selectedOutlineAlpha), + radius = revealProgress * maxRadius - strokePx / 2f, + center = center, + style = Stroke(width = strokePx) ) } if (borderAlpha > 0f) { drawCircle( color = todayBorderColor.copy(alpha = borderAlpha.coerceAtMost(1f)), - radius = size.minDimension / 2f, - center = Offset(size.width / 2f, size.height / 2f), + radius = maxRadius, + center = center, style = Stroke(width = borderAlpha.coerceAtMost(1.5f) * 1.5.dp.toPx()) ) } @@ -247,7 +270,8 @@ fun DayCell( lineHeight = 9.sp, modifier = Modifier .align(Alignment.TopEnd) - .padding(top = 2.dp, end = 4.dp) + .zIndex(1f) + .padding(top = 1.dp, end = 2.dp) ) } } From f63b57eef1b5680dcd172016fabbd92b841ce036 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 18:56:02 +0800 Subject: [PATCH 33/38] =?UTF-8?q?=E8=A7=92=E6=A0=87=E5=8A=A0=20surface=20?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E5=92=AC=E5=BC=80=E6=8F=8F=E8=BE=B9=E5=9C=86?= =?UTF-8?q?,=E4=BB=8A=E6=97=A5=E6=9C=AA=E9=80=89=E4=B8=AD=E6=97=B6?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=8F=8F=E8=BE=B9=E5=8F=AA=E7=95=99=20primar?= =?UTF-8?q?y=20=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 角标(休/班)从只是叠在描边圆之上,改为自带 surface 圆背景: 覆盖身后那段弧,产生"咬开圆环"的视觉断点。 同时 TODAY 状态(今日未被选中)去掉描边圆,文字仍保持 primary 主色, 通过文字颜色标识今天,与选中其他日期时形成更清晰的层次。 --- .../kotlin/plus/rua/project/ui/DayCell.kt | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 628c72a..4ebf09a 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -114,17 +115,6 @@ fun DayCell( } } - val borderAlpha by transition.animateFloat( - transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, - label = "borderAlpha" - ) { state -> - when (state) { - DayCellState.TODAY -> 1.5f - else -> 0f - } - } - - val todayBorderColor = MaterialTheme.colorScheme.primary val selectedOutlineColor = MaterialTheme.colorScheme.primary data class DayAnnotation(val text: String, val isHighlight: Boolean) @@ -229,14 +219,6 @@ fun DayCell( style = Stroke(width = strokePx) ) } - if (borderAlpha > 0f) { - drawCircle( - color = todayBorderColor.copy(alpha = borderAlpha.coerceAtMost(1f)), - radius = maxRadius, - center = center, - style = Stroke(width = borderAlpha.coerceAtMost(1.5f) * 1.5.dp.toPx()) - ) - } } .clickable(onClick = onClick), contentAlignment = Alignment.Center @@ -272,6 +254,8 @@ fun DayCell( .align(Alignment.TopEnd) .zIndex(1f) .padding(top = 1.dp, end = 2.dp) + .background(MaterialTheme.colorScheme.surface, CircleShape) + .padding(horizontal = 2.dp) ) } } From ecf4cf601ea59dac802a22e4c63aeb60d41d4637 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 19:12:18 +0800 Subject: [PATCH 34/38] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E8=BD=AE=E7=8F=AD=20MVP:=E5=B7=A6=E4=B8=8A=E8=A7=92=E8=83=B6?= =?UTF-8?q?=E5=9B=8A=E6=98=BE=E7=A4=BA=E7=8F=AD/=E4=BC=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 ShiftPattern 数据模型,以锚点日期 + 循环序列描述周期性轮班,与法定调休完全独立。 默认配置 2026-05-15 起 [班,班,休,休] 4 天周期,DayCell 左上角渲染胶囊角标。 --- .../plus/rua/project/CalendarViewModel.kt | 13 ++++++++ .../kotlin/plus/rua/project/ShiftPattern.kt | 33 +++++++++++++++++++ .../plus/rua/project/ui/CalendarMonthPage.kt | 3 ++ .../plus/rua/project/ui/CalendarMonthView.kt | 2 ++ .../plus/rua/project/ui/CalendarPager.kt | 3 ++ .../kotlin/plus/rua/project/ui/DayCell.kt | 30 +++++++++++++++++ .../kotlin/plus/rua/project/ui/WeekPager.kt | 3 ++ 7 files changed, 87 insertions(+) create mode 100644 shared/src/commonMain/kotlin/plus/rua/project/ShiftPattern.kt diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index b468972..d81438a 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -77,6 +77,19 @@ class CalendarViewModel( var yearViewYear by mutableStateOf(today.year) internal set + /** + * 个人轮班。与法定节假日完全独立,不受调休影响。 + * MVP 默认:2026-05-15 起,2 班 2 休循环。后续接入设置页与持久化。 + */ + var shiftPattern: ShiftPattern? by mutableStateOf( + ShiftPattern( + anchorDate = LocalDate(2026, 5, 15), + cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF, ShiftKind.OFF) + ) + ) + + fun shiftKindAt(date: LocalDate): ShiftKind? = shiftPattern?.kindAt(date) + /** * 选中指定日期。 * diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ShiftPattern.kt b/shared/src/commonMain/kotlin/plus/rua/project/ShiftPattern.kt new file mode 100644 index 0000000..d34037f --- /dev/null +++ b/shared/src/commonMain/kotlin/plus/rua/project/ShiftPattern.kt @@ -0,0 +1,33 @@ +package plus.rua.project + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil + +/** + * 个人轮班类型。仅区分上班与休息;后续可扩展早/中/晚班、休假等。 + */ +enum class ShiftKind { WORK, OFF } + +/** + * 个人轮班周期。 + * + * 与法定节假日完全独立:周期内某天是 WORK 还是 OFF,只看 + * `(date - anchorDate) mod cycle.size` 在 cycle 中的取值,不受任何节假日/调休影响。 + * + * @param anchorDate 周期基准日,对应 cycle[0] + * @param cycle 一个周期内的班次序列,例如 [WORK, WORK, OFF, OFF] 表示 "2 班 2 休" + * @param name 方案名,用于后续多套方案场景 + */ +data class ShiftPattern( + val anchorDate: LocalDate, + val cycle: List, + val name: String = "默认" +) { + fun kindAt(date: LocalDate): ShiftKind? { + if (cycle.isEmpty()) return null + val diff = anchorDate.daysUntil(date) + val size = cycle.size + val idx = ((diff % size) + size) % size + return cycle[idx] + } +} diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index bcbbc5a..1ecd531 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -22,6 +22,7 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.minus import kotlinx.datetime.number import kotlinx.datetime.plus +import plus.rua.project.ShiftKind /** * 月度日历网格页面,支持两阶段折叠动画。 @@ -50,6 +51,7 @@ fun CalendarMonthPage( collapseProgress: Float, rowHeightPx: Int, effectiveWeeks: Float, + shiftKindAt: (LocalDate) -> ShiftKind?, onRowHeightMeasured: ((Int) -> Unit)? = null, modifier: Modifier = Modifier ) { @@ -152,6 +154,7 @@ fun CalendarMonthPage( isCurrentMonth = dayData.isCurrentMonth, isSelected = dayData.date == selectedDate, isToday = dayData.date == today, + shiftKind = shiftKindAt(dayData.date), onClick = { onDateClick(dayData.date) }, modifier = Modifier.weight(1f) ) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index b7c955b..787bb0f 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -259,6 +259,7 @@ fun CalendarMonthView( } viewModel.selectDate(date) }, + shiftKindAt = { date -> viewModel.shiftKindAt(date) }, modifier = pagerModifier ) } else { @@ -275,6 +276,7 @@ fun CalendarMonthView( collapseProgress = viewModel.collapseProgress, rowHeightPx = rowHeightPx, effectiveWeeks = effectiveWeeks, + shiftKindAt = { date -> viewModel.shiftKindAt(date) }, onRowHeightMeasured = { h -> if (h > 0) rowHeightPx = h }, diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt index 1d29e21..def21e0 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import kotlinx.datetime.number +import plus.rua.project.ShiftKind import kotlin.math.abs /** @@ -42,6 +43,7 @@ fun CalendarPager( collapseProgress: Float, rowHeightPx: Int, effectiveWeeks: Float, + shiftKindAt: (LocalDate) -> ShiftKind?, onRowHeightMeasured: ((Int) -> Unit)? = null, pagerState: PagerState, modifier: Modifier = Modifier @@ -94,6 +96,7 @@ fun CalendarPager( collapseProgress = collapseProgress, rowHeightPx = rowHeightPx, effectiveWeeks = effectiveWeeks, + shiftKindAt = shiftKindAt, onRowHeightMeasured = onRowHeightMeasured, modifier = Modifier.alpha(alpha) ) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 4ebf09a..f95f690 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import com.tyme.solar.SolarDay import kotlinx.datetime.LocalDate +import plus.rua.project.ShiftKind enum class DayCellState { NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY @@ -48,6 +49,7 @@ enum class DayCellState { * @param isCurrentMonth 是否属于当前显示月份 * @param isSelected 是否为选中日期 * @param isToday 是否为今天 + * @param shiftKind 个人轮班类型,左上角胶囊显示;null 表示不显示。与法定调休完全独立。 * @param onClick 点击回调 * @param modifier 外部布局修饰符 */ @@ -57,6 +59,7 @@ fun DayCell( isCurrentMonth: Boolean, isSelected: Boolean, isToday: Boolean, + shiftKind: ShiftKind?, onClick: () -> Unit, modifier: Modifier = Modifier ) { @@ -243,6 +246,33 @@ fun DayCell( ) } } + if (shiftKind != null) { + val shiftBgColor = if (shiftKind == ShiftKind.WORK) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + } + val shiftFgColor = if (shiftKind == ShiftKind.WORK) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onError + } + val shiftLabel = if (shiftKind == ShiftKind.WORK) "班" else "休" + val shiftAlpha = if (isCurrentMonth) 1f else 0.38f + Text( + text = shiftLabel, + color = shiftFgColor.copy(alpha = shiftAlpha), + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + lineHeight = 9.sp, + modifier = Modifier + .align(Alignment.TopStart) + .zIndex(1f) + .padding(top = 1.dp, start = 2.dp) + .background(shiftBgColor.copy(alpha = shiftAlpha), CircleShape) + .padding(horizontal = 2.dp) + ) + } if (holidayBadge != null) { Text( text = holidayBadge, diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt index 45ba90b..338ab2b 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt @@ -18,6 +18,7 @@ import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate import kotlinx.datetime.daysUntil import kotlinx.datetime.plus +import plus.rua.project.ShiftKind import kotlin.math.abs /** @@ -35,6 +36,7 @@ fun WeekPager( today: LocalDate, onDateClick: (LocalDate) -> Unit, onWeekChanged: (LocalDate) -> Unit, + shiftKindAt: (LocalDate) -> ShiftKind?, modifier: Modifier = Modifier ) { val initialWeekMonday = remember { selectedDate.toWeekMonday() } @@ -82,6 +84,7 @@ fun WeekPager( && date.year == selectedDate.year, isSelected = date == selectedDate, isToday = date == today, + shiftKind = shiftKindAt(date), onClick = { onDateClick(date) }, modifier = Modifier.weight(1f) ) From 043bd9824be8a7e307e6f776372936fcdce3ad06 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 19:29:06 +0800 Subject: [PATCH 35/38] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B3=95=E5=AE=9A?= =?UTF-8?q?=E8=B0=83=E4=BC=91=E5=BC=80=E5=85=B3,=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E7=A6=81=E7=94=A8,=E6=8E=92=E7=8F=AD=E6=8E=A5=E7=AE=A1?= =?UTF-8?q?=E5=8F=B3=E4=B8=8A=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ViewModel 增加 showLegalHoliday 状态,默认 false。 关闭时:排班从左上角移到右上角,不显示法定调休角标。 开启时:回到旧布局,左上角=排班、右上角=法定调休。 DayCell 与各 Pager 透传新参数,预留后续接入设置页。 --- .../plus/rua/project/CalendarViewModel.kt | 6 ++++++ .../plus/rua/project/ui/CalendarMonthPage.kt | 4 ++++ .../plus/rua/project/ui/CalendarMonthView.kt | 2 ++ .../plus/rua/project/ui/CalendarPager.kt | 4 ++++ .../kotlin/plus/rua/project/ui/DayCell.kt | 19 +++++++++++++++---- .../kotlin/plus/rua/project/ui/WeekPager.kt | 4 ++++ 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index d81438a..e02108d 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -90,6 +90,12 @@ class CalendarViewModel( fun shiftKindAt(date: LocalDate): ShiftKind? = shiftPattern?.kindAt(date) + /** + * 是否在右上角显示法定调休角标。默认禁用,此时右上角让位给个人排班。 + * 开启后回到旧版布局:左上角=排班,右上角=法定调休。后续接入设置页持久化。 + */ + var showLegalHoliday by mutableStateOf(false) + /** * 选中指定日期。 * diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index 1ecd531..5aac5f4 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -38,6 +38,8 @@ import plus.rua.project.ShiftKind * @param collapseProgress 折叠进度,0f=展开,1f=折叠 * @param rowHeightPx 从外层传入的锁定行高(像素),折叠过程中不变 * @param effectiveWeeks 当前有效行数(含翻页插值),用于计算总高度 + * @param shiftKindAt 日期 → 个人轮班类型的查询闭包 + * @param showLegalHoliday 是否显示法定调休角标。详见 [DayCell] 的同名参数。 * @param onRowHeightMeasured 首次行高测量回调,外层据此锁定行高 * @param modifier 外部布局修饰符 */ @@ -52,6 +54,7 @@ fun CalendarMonthPage( rowHeightPx: Int, effectiveWeeks: Float, shiftKindAt: (LocalDate) -> ShiftKind?, + showLegalHoliday: Boolean, onRowHeightMeasured: ((Int) -> Unit)? = null, modifier: Modifier = Modifier ) { @@ -155,6 +158,7 @@ fun CalendarMonthPage( isSelected = dayData.date == selectedDate, isToday = dayData.date == today, shiftKind = shiftKindAt(dayData.date), + showLegalHoliday = showLegalHoliday, onClick = { onDateClick(dayData.date) }, modifier = Modifier.weight(1f) ) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index 787bb0f..e4ed8cb 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -260,6 +260,7 @@ fun CalendarMonthView( viewModel.selectDate(date) }, shiftKindAt = { date -> viewModel.shiftKindAt(date) }, + showLegalHoliday = viewModel.showLegalHoliday, modifier = pagerModifier ) } else { @@ -277,6 +278,7 @@ fun CalendarMonthView( rowHeightPx = rowHeightPx, effectiveWeeks = effectiveWeeks, shiftKindAt = { date -> viewModel.shiftKindAt(date) }, + showLegalHoliday = viewModel.showLegalHoliday, onRowHeightMeasured = { h -> if (h > 0) rowHeightPx = h }, diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt index def21e0..57d20c0 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -30,6 +30,8 @@ import kotlin.math.abs * @param collapseProgress 折叠进度,0f=展开,1f=折叠 * @param rowHeightPx 锁定行高(像素) * @param effectiveWeeks 当前有效行数(含翻页插值) + * @param shiftKindAt 日期 → 个人轮班类型的查询闭包 + * @param showLegalHoliday 是否显示法定调休角标。详见 [DayCell] 的同名参数。 * @param onRowHeightMeasured 首次行高测量回调 * @param pagerState 外层共享的 PagerState,用于保持翻页状态 * @param modifier 外部布局修饰符 @@ -44,6 +46,7 @@ fun CalendarPager( rowHeightPx: Int, effectiveWeeks: Float, shiftKindAt: (LocalDate) -> ShiftKind?, + showLegalHoliday: Boolean, onRowHeightMeasured: ((Int) -> Unit)? = null, pagerState: PagerState, modifier: Modifier = Modifier @@ -97,6 +100,7 @@ fun CalendarPager( rowHeightPx = rowHeightPx, effectiveWeeks = effectiveWeeks, shiftKindAt = shiftKindAt, + showLegalHoliday = showLegalHoliday, onRowHeightMeasured = onRowHeightMeasured, modifier = Modifier.alpha(alpha) ) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index f95f690..795e1d9 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -49,7 +49,10 @@ enum class DayCellState { * @param isCurrentMonth 是否属于当前显示月份 * @param isSelected 是否为选中日期 * @param isToday 是否为今天 - * @param shiftKind 个人轮班类型,左上角胶囊显示;null 表示不显示。与法定调休完全独立。 + * @param shiftKind 个人轮班类型;null 表示不显示。与法定调休完全独立。 + * @param showLegalHoliday 是否显示法定调休角标。 + * false(默认):排班放右上角,左上角空白,不显示法定调休。 + * true:排班放左上角,法定调休放右上角(旧版布局)。 * @param onClick 点击回调 * @param modifier 外部布局修饰符 */ @@ -60,6 +63,7 @@ fun DayCell( isSelected: Boolean, isToday: Boolean, shiftKind: ShiftKind?, + showLegalHoliday: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) { @@ -259,6 +263,13 @@ fun DayCell( } val shiftLabel = if (shiftKind == ShiftKind.WORK) "班" else "休" val shiftAlpha = if (isCurrentMonth) 1f else 0.38f + // showLegalHoliday=true 时排班让位左上角,法定调休占右上角;否则排班独占右上角 + val shiftAlignment = if (showLegalHoliday) Alignment.TopStart else Alignment.TopEnd + val shiftPadding = if (showLegalHoliday) { + Modifier.padding(top = 1.dp, start = 2.dp) + } else { + Modifier.padding(top = 1.dp, end = 2.dp) + } Text( text = shiftLabel, color = shiftFgColor.copy(alpha = shiftAlpha), @@ -266,14 +277,14 @@ fun DayCell( fontWeight = FontWeight.Bold, lineHeight = 9.sp, modifier = Modifier - .align(Alignment.TopStart) + .align(shiftAlignment) .zIndex(1f) - .padding(top = 1.dp, start = 2.dp) + .then(shiftPadding) .background(shiftBgColor.copy(alpha = shiftAlpha), CircleShape) .padding(horizontal = 2.dp) ) } - if (holidayBadge != null) { + if (showLegalHoliday && holidayBadge != null) { Text( text = holidayBadge, color = holidayBadgeColor.copy(alpha = holidayBadgeAlpha), diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt index 338ab2b..e500203 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt @@ -28,6 +28,8 @@ import kotlin.math.abs * @param today 今天的日期 * @param onDateClick 日期点击回调 * @param onWeekChanged 周切换回调,滑动到新周时触发,参数为该周周一日期 + * @param shiftKindAt 日期 → 个人轮班类型的查询闭包 + * @param showLegalHoliday 是否显示法定调休角标。详见 [DayCell] 的同名参数。 * @param modifier 外部布局修饰符 */ @Composable @@ -37,6 +39,7 @@ fun WeekPager( onDateClick: (LocalDate) -> Unit, onWeekChanged: (LocalDate) -> Unit, shiftKindAt: (LocalDate) -> ShiftKind?, + showLegalHoliday: Boolean, modifier: Modifier = Modifier ) { val initialWeekMonday = remember { selectedDate.toWeekMonday() } @@ -85,6 +88,7 @@ fun WeekPager( isSelected = date == selectedDate, isToday = date == today, shiftKind = shiftKindAt(date), + showLegalHoliday = showLegalHoliday, onClick = { onDateClick(date) }, modifier = Modifier.weight(1f) ) From 6618c1863ab321fd05fdc6f20542e32499a07efa Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 19:32:44 +0800 Subject: [PATCH 36/38] =?UTF-8?q?=E6=8E=92=E7=8F=AD=E5=9C=A8=E5=8F=B3?= =?UTF-8?q?=E4=BE=A7=E6=97=B6=E6=B2=BF=E7=94=A8=E6=B3=95=E5=AE=9A=E8=B0=83?= =?UTF-8?q?=E4=BC=91=E6=A0=B7=E5=BC=8F:surface=20=E5=BA=95=20+=20=E5=BD=A9?= =?UTF-8?q?=E8=89=B2=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 默认布局(showLegalHoliday=false)下,右上角排班从实心胶囊改为 surface 背景圆 + 班=primary/休=error 彩色文字,与开启法定调休 时右上角角标视觉规范一致。左上角(showLegalHoliday=true)保持 实心胶囊样式不变,用于区分两类信息。 --- .../commonMain/kotlin/plus/rua/project/ui/DayCell.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 795e1d9..74fe74b 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -251,19 +251,23 @@ fun DayCell( } } if (shiftKind != null) { - val shiftBgColor = if (shiftKind == ShiftKind.WORK) { + val shiftAccentColor = if (shiftKind == ShiftKind.WORK) { MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.error } - val shiftFgColor = if (shiftKind == ShiftKind.WORK) { + val shiftOnAccentColor = if (shiftKind == ShiftKind.WORK) { MaterialTheme.colorScheme.onPrimary } else { MaterialTheme.colorScheme.onError } val shiftLabel = if (shiftKind == ShiftKind.WORK) "班" else "休" val shiftAlpha = if (isCurrentMonth) 1f else 0.38f - // showLegalHoliday=true 时排班让位左上角,法定调休占右上角;否则排班独占右上角 + // 右上角(默认)沿用法定调休视觉:surface 背景 + 彩色文字; + // 左上角(showLegalHoliday=true 时)用实心胶囊,与右上角法定调休区分。 + val shiftBgColor = + if (showLegalHoliday) shiftAccentColor else MaterialTheme.colorScheme.surface + val shiftFgColor = if (showLegalHoliday) shiftOnAccentColor else shiftAccentColor val shiftAlignment = if (showLegalHoliday) Alignment.TopStart else Alignment.TopEnd val shiftPadding = if (showLegalHoliday) { Modifier.padding(top = 1.dp, start = 2.dp) From 275fc55c79f8f5da831f2efb776321ba2f6ec55d Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 19:37:00 +0800 Subject: [PATCH 37/38] =?UTF-8?q?=E9=80=89=E4=B8=AD=E6=80=81=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E4=BB=8E=20250ms=20=E7=BC=A9=E7=9F=AD=E5=88=B0=20150m?= =?UTF-8?q?s=20=E8=AE=A9=E5=9C=86=E7=8E=AF=E5=A1=AB=E5=85=85=E6=B6=88?= =?UTF-8?q?=E5=A4=B1=E6=9B=B4=E5=88=A9=E8=90=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commonMain/kotlin/plus/rua/project/ui/DayCell.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 74fe74b..5615d93 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -78,7 +78,7 @@ fun DayCell( val transition = updateTransition(targetState = currentState, label = "dayCell") val revealProgress by transition.animateFloat( - transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, + transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, label = "revealProgress" ) { state -> when (state) { @@ -88,7 +88,7 @@ fun DayCell( } val contentColor by transition.animateColor( - transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, + transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, label = "contentColor" ) { state -> when (state) { @@ -102,7 +102,7 @@ fun DayCell( // 选中今天:实心填充 primaryContainer;其他状态不填充。 val selectedFillColor by transition.animateColor( - transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, + transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, label = "selectedFillColor" ) { state -> when (state) { @@ -113,7 +113,7 @@ fun DayCell( // 选中非今天:绘制描边圆,避免遮挡右上角角标。 val selectedOutlineAlpha by transition.animateFloat( - transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, + transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, label = "selectedOutlineAlpha" ) { state -> when (state) { @@ -167,7 +167,7 @@ fun DayCell( } val lunarColor by transition.animateColor( - transitionSpec = { tween(250, easing = FastOutSlowInEasing) }, + transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, label = "lunarColor" ) { state -> if (annotation.isHighlight) { From c95f118daf782df8e2ea6fe3f218a3009461d792 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Sat, 16 May 2026 19:40:47 +0800 Subject: [PATCH 38/38] =?UTF-8?q?DayCell=20=E5=85=B3=E9=97=AD=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=20ripple,=E6=B6=88=E9=99=A4=E9=80=89=E4=B8=AD?= =?UTF-8?q?=E5=90=8E=E6=AE=8B=E7=95=99=E7=9A=84=E7=81=B0=E8=89=B2=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clickable 默认 indication 会在 release 后慢慢淡出灰色波纹, 选中态视觉已由 reveal 圆形动画承担,关闭默认 indication 即可 让点击反馈立即结束,不留灰色残影。 --- .../src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index 5615d93..b77dac0 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio @@ -227,7 +228,11 @@ fun DayCell( ) } } - .clickable(onClick = onClick), + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick + ), contentAlignment = Alignment.Center ) { Column(