新增个人轮班 MVP:左上角胶囊显示班/休

新增 ShiftPattern 数据模型,以锚点日期 + 循环序列描述周期性轮班,与法定调休完全独立。
默认配置 2026-05-15 起 [班,班,休,休] 4 天周期,DayCell 左上角渲染胶囊角标。
This commit is contained in:
meyou 2026-05-16 19:12:18 +08:00
parent f63b57eef1
commit ecf4cf601e
7 changed files with 87 additions and 0 deletions

View File

@ -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)
/**
* 选中指定日期
*

View File

@ -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<ShiftKind>,
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]
}
}

View File

@ -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)
)

View File

@ -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
},

View File

@ -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)
)

View File

@ -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,

View File

@ -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)
)