feat(shift): swipeable mini calendar via HorizontalPager

迷你月历接入 HorizontalPager,支持左右滑动翻月:
- 复用主日历 START_PAGE/pageToYearMonth/yearMonthToPage 工具
- viewYear/viewMonth 由 pagerState.currentPage 派生
- 箭头按钮改为 animateScrollToPage
- DatePicker 选月后 animateScrollToPage 跳转
- 网格提取为 MonthGrid 单页组件
This commit is contained in:
xfy 2026-07-09 17:07:38 +08:00
parent 1cf950817e
commit 7b1290dd66

View File

@ -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,
// 滑动翻页区域:每页一个月的网格
HorizontalPager(
state = pagerState,
beyondViewportPageCount = 0,
modifier = Modifier.fillMaxWidth()
) { page ->
val (year, month) = pageToYearMonth(page, initialYear, initialMonth)
MonthGrid(
year = year,
month = month,
pattern = pattern,
onClick = { onPatternChange(toggleOverride(pattern, date)) },
onLongClick = { onPatternChange(toggleFlipAndRephase(pattern, date)) },
modifier = Modifier.weight(1f)
onPatternChange = onPatternChange,
modifier = Modifier.fillMaxWidth()
)
} else {
Box(Modifier.weight(1f).aspectRatio(1f))
}
}
}
}
}
}
@ -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
*