From ef55ffcf6452d8a654bfc5d9ecc92b0cf809e164 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 6 Jul 2026 18:34:12 +0800 Subject: [PATCH] feat(shift): inject ShiftPatternStorage into CalendarViewModel --- .../plus/rua/project/CalendarViewModel.kt | 30 ++++-- .../plus/rua/project/CalendarViewModelTest.kt | 98 +++++++++++++++++++ 2 files changed, 120 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt index 00d0473..5256691 100644 --- a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt @@ -59,7 +59,8 @@ data class CalendarUiState( * @param clock 时钟源,默认系统时钟;测试时可注入固定时钟 */ class CalendarViewModel( - private val clock: Clock = Clock.System + private val clock: Clock = Clock.System, + private val shiftStorage: ShiftPatternStorage? = null ) : ViewModel() { private val today: LocalDate = clock.todayIn(TimeZone.currentSystemDefault()) @@ -112,17 +113,30 @@ class CalendarViewModel( /** * 个人轮班。与法定节假日完全独立,不受调休影响。 - * MVP 默认:2026-05-15 起,2 班 2 休循环。后续接入设置页与持久化。 + * 从 [shiftStorage] 加载;storage 为空或未存时回退到 [DEFAULT_PATTERN]。 */ - private val _shiftPattern = MutableStateFlow( - ShiftPattern( + private val _shiftPattern = MutableStateFlow(loadShiftPattern()) + val shiftPattern: StateFlow = _shiftPattern.asStateFlow() + + private fun loadShiftPattern(): ShiftPattern = + shiftStorage?.load() ?: DEFAULT_PATTERN + + /** + * 设置页返回后调用,从 storage 重新加载,立即生效。 + */ + fun refreshShiftPattern() { + _shiftPattern.value = loadShiftPattern() + } + + fun shiftKindAt(date: LocalDate): ShiftKind? = shiftPattern.value?.kindAt(date) + + companion object { + /** 默认轮班:2026-05-15 起,2 班 2 休循环。 */ + val DEFAULT_PATTERN = ShiftPattern( anchorDate = LocalDate(2026, 5, 15), cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF, ShiftKind.OFF) ) - ) - val shiftPattern: StateFlow = _shiftPattern.asStateFlow() - - fun shiftKindAt(date: LocalDate): ShiftKind? = shiftPattern.value?.kindAt(date) + } /** * 是否在右上角显示法定调休角标。默认禁用,此时右上角让位给个人排班。 diff --git a/core/src/test/kotlin/plus/rua/project/CalendarViewModelTest.kt b/core/src/test/kotlin/plus/rua/project/CalendarViewModelTest.kt index 065b97c..e8dd021 100644 --- a/core/src/test/kotlin/plus/rua/project/CalendarViewModelTest.kt +++ b/core/src/test/kotlin/plus/rua/project/CalendarViewModelTest.kt @@ -1,9 +1,11 @@ package plus.rua.project +import android.content.SharedPreferences import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.number +import plus.rua.project.ShiftKind import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -127,4 +129,100 @@ class CalendarViewModelTest { val selectedCell = days.first { it.isSelected } assertEquals(15, selectedCell.date.day) } + + // ---- shiftPattern: 默认值与 refresh ---- + + @Test + fun shiftPattern_noStorage_returnsDefault() { + val vm = createViewModel() // 不传 storage + assertEquals(ShiftKind.WORK, vm.shiftKindAt(LocalDate(2026, 5, 15))) + assertEquals(ShiftKind.OFF, vm.shiftKindAt(LocalDate(2026, 5, 17))) + } + + @Test + fun refreshShiftPattern_reloadsFromStorage() { + val prefs = CalendarVmTestPrefs() + val storage = ShiftPatternStorage(prefs) + // storage 里存一个 1班1休,锚点 2026-01-01 + storage.save( + ShiftPattern( + anchorDate = LocalDate(2026, 1, 1), + cycle = listOf(ShiftKind.WORK, ShiftKind.OFF) + ) + ) + val vm = CalendarViewModel(clock = testClock, shiftStorage = storage) + // 初始即从 storage 读 + assertEquals(ShiftKind.WORK, vm.shiftKindAt(LocalDate(2026, 1, 1))) + assertEquals(ShiftKind.OFF, vm.shiftKindAt(LocalDate(2026, 1, 2))) + } +} + +private class CalendarVmTestPrefs : SharedPreferences { + + private val data = mutableMapOf() + + override fun getAll(): Map = data.toMap() + + override fun getString(key: String, defValue: String?): String? = + data[key] as? String ?: defValue + + override fun getStringSet(key: String, defValues: Set?): Set? = + data[key] as? Set ?: defValues + + override fun getInt(key: String, defValue: Int): Int = + data[key] as? Int ?: defValue + + override fun getLong(key: String, defValue: Long): Long = + data[key] as? Long ?: defValue + + override fun getFloat(key: String, defValue: Float): Float = + data[key] as? Float ?: defValue + + override fun getBoolean(key: String, defValue: Boolean): Boolean = + data[key] as? Boolean ?: defValue + + override fun contains(key: String): Boolean = data.containsKey(key) + + override fun edit(): SharedPreferences.Editor = CalendarVmTestPrefsEditor(data) + + override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {} + + override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {} +} + +private class CalendarVmTestPrefsEditor(private val data: MutableMap) : SharedPreferences.Editor { + + private val pending = mutableMapOf() + private var clearPending = false + + override fun putString(key: String, value: String?): SharedPreferences.Editor = apply { + pending[key] = value + } + + override fun putStringSet(key: String, values: Set?): SharedPreferences.Editor = apply { + pending[key] = values + } + + override fun putInt(key: String, value: Int): SharedPreferences.Editor = apply { pending[key] = value } + override fun putLong(key: String, value: Long): SharedPreferences.Editor = apply { pending[key] = value } + override fun putFloat(key: String, value: Float): SharedPreferences.Editor = apply { pending[key] = value } + override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = apply { pending[key] = value } + + override fun remove(key: String): SharedPreferences.Editor = apply { pending[key] = null } + + override fun clear(): SharedPreferences.Editor = apply { clearPending = true } + + override fun commit(): Boolean { + apply() + return true + } + + override fun apply() { + if (clearPending) { + data.clear() + clearPending = false + } + data.putAll(pending) + pending.clear() + } }