feat: 新建记录标题预填拍摄日期并支持联动

This commit is contained in:
xfy 2026-07-20 17:23:33 +08:00
parent 94e81bffb1
commit 0d83fef3a9
2 changed files with 88 additions and 2 deletions

View File

@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
import java.io.File import java.io.File
import kotlin.time.Clock import kotlin.time.Clock
@ -58,6 +59,9 @@ class RecordEditViewModel(
private val _uiState = MutableStateFlow(RecordEditUiState()) private val _uiState = MutableStateFlow(RecordEditUiState())
val uiState: StateFlow<RecordEditUiState> = _uiState.asStateFlow() val uiState: StateFlow<RecordEditUiState> = _uiState.asStateFlow()
/** 用户是否手动编辑过标题;为 false 时标题随拍摄日期联动 */
private var titleManuallyEdited = false
init { init {
if (recordId != null) { if (recordId != null) {
loadExistingRecord(recordId) loadExistingRecord(recordId)
@ -69,11 +73,14 @@ class RecordEditViewModel(
private fun initNewRecord() { private fun initNewRecord() {
requireNotNull(photoPath) { "新建模式必须提供 photoPath" } requireNotNull(photoPath) { "新建模式必须提供 photoPath" }
val file = File(photoPath) val file = File(photoPath)
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
_uiState.value = RecordEditUiState( _uiState.value = RecordEditUiState(
loading = false, loading = false,
title = formatLocalDate(today),
shootDate = today,
photoUri = "file://${file.absolutePath}", photoUri = "file://${file.absolutePath}",
photoAbsolutePath = file.absolutePath, photoAbsolutePath = file.absolutePath,
canSave = false, canSave = true,
isExistingRecord = false isExistingRecord = false
) )
} }
@ -83,6 +90,7 @@ class RecordEditViewModel(
val record = repository.observeById(id).first() val record = repository.observeById(id).first()
if (record != null) { if (record != null) {
val absFile = repository.absoluteFileOf(record.photoPath) val absFile = repository.absoluteFileOf(record.photoPath)
titleManuallyEdited = true
_uiState.value = RecordEditUiState( _uiState.value = RecordEditUiState(
loading = false, loading = false,
title = record.title, title = record.title,
@ -102,6 +110,7 @@ class RecordEditViewModel(
} }
fun onTitleChange(value: String) { fun onTitleChange(value: String) {
titleManuallyEdited = true
_uiState.update { it.copy(title = value, canSave = value.isNotBlank() && it.photoAbsolutePath != null) } _uiState.update { it.copy(title = value, canSave = value.isNotBlank() && it.photoAbsolutePath != null) }
} }
@ -110,7 +119,13 @@ class RecordEditViewModel(
} }
fun onShootDateChange(date: LocalDate) { 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) { 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')}"
}

View File

@ -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<DateRecord?> = flowOf(null)
override fun getAllFlow(): Flow<List<DateRecord>> = flowOf(emptyList())
override suspend fun insert(record: DateRecord): Long = 0
override suspend fun update(record: DateRecord) = Unit
override suspend fun deleteByIds(ids: List<Long>): Int = 0
}