From 0d83fef3a986831f95e75a22c2458338103766a3 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 20 Jul 2026 17:23:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=BB=BA=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E9=A2=84=E5=A1=AB=E6=8B=8D=E6=91=84=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E5=B9=B6=E6=94=AF=E6=8C=81=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plus/rua/project/RecordEditViewModel.kt | 28 ++++++++- .../rua/project/RecordEditViewModelTest.kt | 62 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 core/src/test/kotlin/plus/rua/project/RecordEditViewModelTest.kt diff --git a/core/src/main/kotlin/plus/rua/project/RecordEditViewModel.kt b/core/src/main/kotlin/plus/rua/project/RecordEditViewModel.kt index 57fab01..72c70d3 100644 --- a/core/src/main/kotlin/plus/rua/project/RecordEditViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/RecordEditViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone +import kotlinx.datetime.number import kotlinx.datetime.todayIn import java.io.File import kotlin.time.Clock @@ -58,6 +59,9 @@ class RecordEditViewModel( private val _uiState = MutableStateFlow(RecordEditUiState()) val uiState: StateFlow = _uiState.asStateFlow() + /** 用户是否手动编辑过标题;为 false 时标题随拍摄日期联动 */ + private var titleManuallyEdited = false + init { if (recordId != null) { loadExistingRecord(recordId) @@ -69,11 +73,14 @@ class RecordEditViewModel( private fun initNewRecord() { requireNotNull(photoPath) { "新建模式必须提供 photoPath" } val file = File(photoPath) + val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) _uiState.value = RecordEditUiState( loading = false, + title = formatLocalDate(today), + shootDate = today, photoUri = "file://${file.absolutePath}", photoAbsolutePath = file.absolutePath, - canSave = false, + canSave = true, isExistingRecord = false ) } @@ -83,6 +90,7 @@ class RecordEditViewModel( val record = repository.observeById(id).first() if (record != null) { val absFile = repository.absoluteFileOf(record.photoPath) + titleManuallyEdited = true _uiState.value = RecordEditUiState( loading = false, title = record.title, @@ -102,6 +110,7 @@ class RecordEditViewModel( } fun onTitleChange(value: String) { + titleManuallyEdited = true _uiState.update { it.copy(title = value, canSave = value.isNotBlank() && it.photoAbsolutePath != null) } } @@ -110,7 +119,13 @@ class RecordEditViewModel( } fun onShootDateChange(date: LocalDate) { - _uiState.update { it.copy(shootDate = date) } + _uiState.update { + if (titleManuallyEdited) { + it.copy(shootDate = date) + } else { + it.copy(shootDate = date, title = formatLocalDate(date)) + } + } } fun onLinkedDateChange(date: LocalDate) { @@ -155,3 +170,12 @@ class RecordEditViewModel( } } } + +/** + * 将日期格式化为 `yyyy-MM-dd`,用于新建记录的默认标题和编辑页的日期显示。 + * + * @param date 待格式化的日期 + */ +internal fun formatLocalDate(date: LocalDate): String { + return "${date.year}-${date.month.number.toString().padStart(2, '0')}-${date.day.toString().padStart(2, '0')}" +} diff --git a/core/src/test/kotlin/plus/rua/project/RecordEditViewModelTest.kt b/core/src/test/kotlin/plus/rua/project/RecordEditViewModelTest.kt new file mode 100644 index 0000000..dd7d14c --- /dev/null +++ b/core/src/test/kotlin/plus/rua/project/RecordEditViewModelTest.kt @@ -0,0 +1,62 @@ +package plus.rua.project + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.datetime.LocalDate +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * 新建记录标题预填与拍摄日期联动的单元测试。 + * + * 新建模式的被测路径不会触发 DAO 调用,[FakeDateRecordDao] 返回空数据即可。 + */ +class RecordEditViewModelTest { + + private fun newViewModel(): RecordEditViewModel { + val repository = DateRecorderRepository(FakeDateRecordDao(), File("build/tmp/record_edit_test")) + return RecordEditViewModel(repository, photoPath = "/tmp/fake_photo.jpg", recordId = null) + } + + @Test + fun newRecord_prefillsTitleWithShootDate_andCanSave() { + val vm = newViewModel() + val state = vm.uiState.value + assertEquals(formatLocalDate(state.shootDate), state.title) + assertTrue(state.canSave) + } + + @Test + fun shootDateChange_updatesTitle_whenTitleNotManuallyEdited() { + val vm = newViewModel() + vm.onShootDateChange(LocalDate(2026, 3, 5)) + assertEquals("2026-03-05", vm.uiState.value.title) + assertTrue(vm.uiState.value.canSave) + } + + @Test + fun shootDateChange_keepsTitle_whenTitleManuallyEdited() { + val vm = newViewModel() + vm.onTitleChange("我的记录") + vm.onShootDateChange(LocalDate(2026, 3, 5)) + assertEquals("我的记录", vm.uiState.value.title) + } + + @Test + fun titleCleared_disablesSave() { + val vm = newViewModel() + vm.onTitleChange("") + assertFalse(vm.uiState.value.canSave) + } +} + +private class FakeDateRecordDao : DateRecordDao { + override fun getByIdFlow(id: Long): Flow = flowOf(null) + override fun getAllFlow(): Flow> = flowOf(emptyList()) + override suspend fun insert(record: DateRecord): Long = 0 + override suspend fun update(record: DateRecord) = Unit + override suspend fun deleteByIds(ids: List): Int = 0 +}