From 7b1290dd66fb7925857124446fa7a69f88670c37 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 9 Jul 2026 17:07:38 +0800 Subject: [PATCH] feat(shift): swipeable mini calendar via HorizontalPager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迷你月历接入 HorizontalPager,支持左右滑动翻月: - 复用主日历 START_PAGE/pageToYearMonth/yearMonthToPage 工具 - viewYear/viewMonth 由 pagerState.currentPage 派生 - 箭头按钮改为 animateScrollToPage - DatePicker 选月后 animateScrollToPage 跳转 - 网格提取为 MonthGrid 单页组件 --- .../plus/rua/project/ui/ShiftCalendarGrid.kt | 107 +++++++++++++----- 1 file changed, 78 insertions(+), 29 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/ui/ShiftCalendarGrid.kt b/core/src/main/kotlin/plus/rua/project/ui/ShiftCalendarGrid.kt index 65b3c20..5751382 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/ShiftCalendarGrid.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/ShiftCalendarGrid.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -28,9 +30,11 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,6 +43,7 @@ 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.coroutines.launch import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate import kotlinx.datetime.Month @@ -75,14 +80,19 @@ fun ShiftCalendarGrid( modifier: Modifier = Modifier ) { val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) } - var viewYear by remember { mutableStateOf(today.year) } - var viewMonth by remember { mutableStateOf(today.month.number) } + val initialYear = remember { today.year } + val initialMonth = remember { today.month.number } var showMonthPicker by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() - // 复用主日历的网格计算(周一为首日,动态行数 4/5/6) - val gridInfo = remember(viewYear, viewMonth) { getMonthGridInfo(viewYear, viewMonth) } - val firstWeekdayOffset = gridInfo.offset - val rows = gridInfo.rows + // Pager 居中无限页,与主日历一致;viewYear/viewMonth 由当前页派生 + val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE }) + val viewYear by remember { + derivedStateOf { pageToYearMonth(pagerState.currentPage, initialYear, initialMonth).first } + } + val viewMonth by remember { + derivedStateOf { pageToYearMonth(pagerState.currentPage, initialYear, initialMonth).second } + } Card( modifier = modifier.fillMaxWidth(), @@ -97,7 +107,7 @@ fun ShiftCalendarGrid( verticalAlignment = Alignment.CenterVertically ) { IconButton(onClick = { - if (viewMonth == 1) { viewMonth = 12; viewYear -= 1 } else viewMonth -= 1 + coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) } }) { Icon(Icons.Filled.ChevronLeft, contentDescription = "上个月") } @@ -108,7 +118,7 @@ fun ShiftCalendarGrid( modifier = Modifier.clickable { showMonthPicker = true } ) IconButton(onClick = { - if (viewMonth == 12) { viewMonth = 1; viewYear += 1 } else viewMonth += 1 + coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } }) { Icon(Icons.Filled.ChevronRight, contentDescription = "下个月") } @@ -127,25 +137,20 @@ fun ShiftCalendarGrid( } } - (0 until rows).forEach { row -> - Row(Modifier.fillMaxWidth()) { - (0 until 7).forEach { col -> - val cellIndex = row * 7 + col - val dayNum = cellIndex - firstWeekdayOffset + 1 - if (dayNum in 1..gridInfo.daysInMonth) { - val date = LocalDate(viewYear, Month(viewMonth), dayNum) - ShiftDayCell( - date = date, - pattern = pattern, - onClick = { onPatternChange(toggleOverride(pattern, date)) }, - onLongClick = { onPatternChange(toggleFlipAndRephase(pattern, date)) }, - modifier = Modifier.weight(1f) - ) - } else { - Box(Modifier.weight(1f).aspectRatio(1f)) - } - } - } + // 滑动翻页区域:每页一个月的网格 + HorizontalPager( + state = pagerState, + beyondViewportPageCount = 0, + modifier = Modifier.fillMaxWidth() + ) { page -> + val (year, month) = pageToYearMonth(page, initialYear, initialMonth) + MonthGrid( + year = year, + month = month, + pattern = pattern, + onPatternChange = onPatternChange, + modifier = Modifier.fillMaxWidth() + ) } } } @@ -161,8 +166,10 @@ fun ShiftCalendarGrid( TextButton(onClick = { datePickerState.selectedDateMillis?.let { millis -> val picked = millis.toLocalDate() - viewYear = picked.year - viewMonth = picked.month.number + val targetPage = yearMonthToPage( + picked.year, picked.month.number, initialYear, initialMonth + ) + coroutineScope.launch { pagerState.animateScrollToPage(targetPage) } } showMonthPicker = false }) { Text("确定") } @@ -176,6 +183,48 @@ fun ShiftCalendarGrid( } } +/** + * 迷你月历的单页网格。复用主日历 [getMonthGridInfo] 计算行数与偏移。 + * + * @param year 年 + * @param month 月(1-12) + * @param pattern 当前轮班配置(只读) + * @param onPatternChange 修改后的新 pattern 回调 + * @param modifier 外部布局修饰符 + */ +@Composable +private fun MonthGrid( + year: Int, + month: Int, + pattern: ShiftPattern, + onPatternChange: (ShiftPattern) -> Unit, + modifier: Modifier = Modifier +) { + val gridInfo = remember(year, month) { getMonthGridInfo(year, month) } + Column(modifier) { + (0 until gridInfo.rows).forEach { row -> + Row(Modifier.fillMaxWidth()) { + (0 until 7).forEach { col -> + val cellIndex = row * 7 + col + val dayNum = cellIndex - gridInfo.offset + 1 + if (dayNum in 1..gridInfo.daysInMonth) { + val date = LocalDate(year, Month(month), dayNum) + ShiftDayCell( + date = date, + pattern = pattern, + onClick = { onPatternChange(toggleOverride(pattern, date)) }, + onLongClick = { onPatternChange(toggleFlipAndRephase(pattern, date)) }, + modifier = Modifier.weight(1f) + ) + } else { + Box(Modifier.weight(1f).aspectRatio(1f)) + } + } + } + } + } +} + /** * 翻转某天的班/休(单日 override)。翻转后若与基础周期值一致,则移除 override。 *