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.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -28,9 +30,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import kotlinx.datetime.DatePeriod import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month import kotlinx.datetime.Month
@ -75,14 +80,19 @@ fun ShiftCalendarGrid(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) } val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) }
var viewYear by remember { mutableStateOf(today.year) } val initialYear = remember { today.year }
var viewMonth by remember { mutableStateOf(today.month.number) } val initialMonth = remember { today.month.number }
var showMonthPicker by remember { mutableStateOf(false) } var showMonthPicker by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
// 复用主日历的网格计算(周一为首日,动态行数 4/5/6) // Pager 居中无限页,与主日历一致;viewYear/viewMonth 由当前页派生
val gridInfo = remember(viewYear, viewMonth) { getMonthGridInfo(viewYear, viewMonth) } val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE })
val firstWeekdayOffset = gridInfo.offset val viewYear by remember {
val rows = gridInfo.rows derivedStateOf { pageToYearMonth(pagerState.currentPage, initialYear, initialMonth).first }
}
val viewMonth by remember {
derivedStateOf { pageToYearMonth(pagerState.currentPage, initialYear, initialMonth).second }
}
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@ -97,7 +107,7 @@ fun ShiftCalendarGrid(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { IconButton(onClick = {
if (viewMonth == 1) { viewMonth = 12; viewYear -= 1 } else viewMonth -= 1 coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) }
}) { }) {
Icon(Icons.Filled.ChevronLeft, contentDescription = "上个月") Icon(Icons.Filled.ChevronLeft, contentDescription = "上个月")
} }
@ -108,7 +118,7 @@ fun ShiftCalendarGrid(
modifier = Modifier.clickable { showMonthPicker = true } modifier = Modifier.clickable { showMonthPicker = true }
) )
IconButton(onClick = { IconButton(onClick = {
if (viewMonth == 12) { viewMonth = 1; viewYear += 1 } else viewMonth += 1 coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
}) { }) {
Icon(Icons.Filled.ChevronRight, contentDescription = "下个月") Icon(Icons.Filled.ChevronRight, contentDescription = "下个月")
} }
@ -127,25 +137,20 @@ fun ShiftCalendarGrid(
} }
} }
(0 until rows).forEach { row -> // 滑动翻页区域:每页一个月的网格
Row(Modifier.fillMaxWidth()) { HorizontalPager(
(0 until 7).forEach { col -> state = pagerState,
val cellIndex = row * 7 + col beyondViewportPageCount = 0,
val dayNum = cellIndex - firstWeekdayOffset + 1 modifier = Modifier.fillMaxWidth()
if (dayNum in 1..gridInfo.daysInMonth) { ) { page ->
val date = LocalDate(viewYear, Month(viewMonth), dayNum) val (year, month) = pageToYearMonth(page, initialYear, initialMonth)
ShiftDayCell( MonthGrid(
date = date, year = year,
pattern = pattern, month = month,
onClick = { onPatternChange(toggleOverride(pattern, date)) }, pattern = pattern,
onLongClick = { onPatternChange(toggleFlipAndRephase(pattern, date)) }, onPatternChange = onPatternChange,
modifier = Modifier.weight(1f) modifier = Modifier.fillMaxWidth()
) )
} else {
Box(Modifier.weight(1f).aspectRatio(1f))
}
}
}
} }
} }
} }
@ -161,8 +166,10 @@ fun ShiftCalendarGrid(
TextButton(onClick = { TextButton(onClick = {
datePickerState.selectedDateMillis?.let { millis -> datePickerState.selectedDateMillis?.let { millis ->
val picked = millis.toLocalDate() val picked = millis.toLocalDate()
viewYear = picked.year val targetPage = yearMonthToPage(
viewMonth = picked.month.number picked.year, picked.month.number, initialYear, initialMonth
)
coroutineScope.launch { pagerState.animateScrollToPage(targetPage) }
} }
showMonthPicker = false showMonthPicker = false
}) { Text("确定") } }) { 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 * 翻转某天的班/(单日 override)翻转后若与基础周期值一致,则移除 override
* *