diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c048a2d..20bb341 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,11 +13,11 @@ android { applicationId = "com.healthflow.app" minSdk = 26 targetSdk = 35 - versionCode = 116 - versionName = "3.1.6" + versionCode = 118 + versionName = "3.1.8" buildConfigField("String", "API_BASE_URL", "\"https://health.amos.us.kg/api/\"") - buildConfigField("int", "VERSION_CODE", "116") + buildConfigField("int", "VERSION_CODE", "118") } signingConfigs { diff --git a/android/app/src/main/java/com/healthflow/app/ui/navigation/Navigation.kt b/android/app/src/main/java/com/healthflow/app/ui/navigation/Navigation.kt index 4f3ce51..3b89958 100644 --- a/android/app/src/main/java/com/healthflow/app/ui/navigation/Navigation.kt +++ b/android/app/src/main/java/com/healthflow/app/ui/navigation/Navigation.kt @@ -167,12 +167,14 @@ fun MainNavigation( } composable(Routes.CREATE_EPOCH) { + val latestWeight by epochViewModel.latestWeight.collectAsState() CreateEpochScreen( isLoading = isLoading, error = error, + latestWeight = latestWeight, onBack = { navController.popBackStack() }, - onCreateEpoch = { name, startDate, endDate -> - epochViewModel.createEpoch(name, startDate, endDate) { + onCreateEpoch = { name, initialWeight, targetWeight, startDate, endDate -> + epochViewModel.createEpoch(name, initialWeight, targetWeight, startDate, endDate) { epochViewModel.loadAll() navController.popBackStack() } diff --git a/android/app/src/main/java/com/healthflow/app/ui/screen/CreateEpochScreen.kt b/android/app/src/main/java/com/healthflow/app/ui/screen/CreateEpochScreen.kt index cd464c7..d13a65d 100644 --- a/android/app/src/main/java/com/healthflow/app/ui/screen/CreateEpochScreen.kt +++ b/android/app/src/main/java/com/healthflow/app/ui/screen/CreateEpochScreen.kt @@ -3,7 +3,9 @@ package com.healthflow.app.ui.screen import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material3.* @@ -23,10 +25,14 @@ import java.time.format.DateTimeFormatter fun CreateEpochScreen( isLoading: Boolean, error: String?, + latestWeight: Double?, onBack: () -> Unit, - onCreateEpoch: (name: String, startDate: String, endDate: String) -> Unit + onCreateEpoch: (name: String, initialWeight: Double, targetWeight: Double, startDate: String, endDate: String) -> Unit ) { var name by remember { mutableStateOf("") } + var initialWeightText by remember { mutableStateOf(latestWeight?.toString() ?: "") } + var targetWeightLossText by remember { mutableStateOf("") } + var targetWeightText by remember { mutableStateOf("") } var startDate by remember { mutableStateOf(null) } var endDate by remember { mutableStateOf(null) } @@ -34,47 +40,85 @@ fun CreateEpochScreen( var showEndDatePicker by remember { mutableStateOf(false) } val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + + // 初始化时设置默认体重 + LaunchedEffect(latestWeight) { + if (latestWeight != null && initialWeightText.isEmpty()) { + initialWeightText = latestWeight.toString() + } + } + + // 计算目标体重和目标减重的相互换算 + fun updateTargetWeight(weightLoss: Double) { + val initial = initialWeightText.toDoubleOrNull() ?: 0.0 + if (initial > 0 && weightLoss > 0) { + val target = initial - weightLoss + if (target > 0) { + targetWeightText = String.format("%.1f", target) + } + } + } + + fun updateTargetWeightLoss(targetWeight: Double) { + val initial = initialWeightText.toDoubleOrNull() ?: 0.0 + if (initial > 0 && targetWeight > 0 && targetWeight < initial) { + val loss = initial - targetWeight + targetWeightLossText = String.format("%.1f", loss) + } + } Column( modifier = Modifier .fillMaxSize() .background(Color.White) .statusBarsPadding() - .padding(horizontal = 24.dp) ) { - Spacer(modifier = Modifier.height(8.dp)) + // 固定顶部区域 + Column( + modifier = Modifier.padding(horizontal = 24.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // 返回按钮 - 负边距让图标视觉上对齐文字 + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "返回", + tint = Slate900, + modifier = Modifier + .offset(x = (-8).dp) + .size(32.dp) + .clickable(onClick = onBack) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 标题 + Text( + text = "开启新纪元", + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + letterSpacing = (-0.5).sp, + color = Slate900 + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "为你的健康阶段设定一个开端", + fontSize = 14.sp, + color = Slate500 + ) + } - // 返回按钮 - 负边距让图标视觉上对齐文字 - Icon( - Icons.AutoMirrored.Filled.KeyboardArrowLeft, - contentDescription = "返回", - tint = Slate900, + // 可滚动内容区域 + Column( modifier = Modifier - .offset(x = (-8).dp) - .size(32.dp) - .clickable(onClick = onBack) - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // 标题 - Text( - text = "开启新纪元", - fontSize = 28.sp, - fontWeight = FontWeight.Bold, - letterSpacing = (-0.5).sp, - color = Slate900 - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = "为你的健康阶段设定一个开端", - fontSize = 14.sp, - color = Slate500 - ) - - Spacer(modifier = Modifier.height(32.dp)) + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(32.dp)) // 纪元名称 Text( @@ -103,6 +147,106 @@ fun CreateEpochScreen( Spacer(modifier = Modifier.height(24.dp)) + // 初始体重 + Text( + text = "初始体重 (kg)", + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + color = Slate500, + letterSpacing = 0.5.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = initialWeightText, + onValueChange = { + initialWeightText = it + // 当初始体重改变时,重新计算目标体重 + val weightLoss = targetWeightLossText.toDoubleOrNull() + if (weightLoss != null && weightLoss > 0) { + updateTargetWeight(weightLoss) + } + }, + placeholder = { Text("请输入初始体重", color = Slate400, fontSize = 16.sp) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Slate900, + unfocusedBorderColor = Slate200, + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White + ), + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontSize = 16.sp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // 目标减重 + Text( + text = "目标减重 (kg)", + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + color = Slate500, + letterSpacing = 0.5.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = targetWeightLossText, + onValueChange = { + targetWeightLossText = it + val weightLoss = it.toDoubleOrNull() + if (weightLoss != null && weightLoss > 0) { + updateTargetWeight(weightLoss) + } + }, + placeholder = { Text("例如:10", color = Slate400, fontSize = 16.sp) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Slate900, + unfocusedBorderColor = Slate200, + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White + ), + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontSize = 16.sp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // 目标体重 + Text( + text = "目标体重 (kg)", + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + color = Slate500, + letterSpacing = 0.5.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = targetWeightText, + onValueChange = { + targetWeightText = it + val targetWeight = it.toDoubleOrNull() + if (targetWeight != null && targetWeight > 0) { + updateTargetWeightLoss(targetWeight) + } + }, + placeholder = { Text("例如:70", color = Slate400, fontSize = 16.sp) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Slate900, + unfocusedBorderColor = Slate200, + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White + ), + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontSize = 16.sp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + // 起始日期 Text( text = "起始日期", @@ -133,52 +277,75 @@ fun CreateEpochScreen( onClick = { showEndDatePicker = true } ) - // 错误提示 - error?.let { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = it, - color = ErrorRed, - fontSize = 14.sp - ) + // 底部留白,避免被按钮遮挡 + Spacer(modifier = Modifier.height(16.dp)) } - Spacer(modifier = Modifier.height(40.dp)) - - // 确认按钮 - Button( - onClick = { - if (startDate != null && endDate != null) { - onCreateEpoch( - name.ifEmpty { "新纪元" }, - startDate!!.format(dateFormatter), - endDate!!.format(dateFormatter) - ) - } - }, + // 底部按钮区域(固定) + Column( modifier = Modifier .fillMaxWidth() - .height(56.dp), - enabled = startDate != null && endDate != null && !isLoading, - shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Slate900, - disabledContainerColor = Slate300 - ) + .padding(horizontal = 24.dp) + .padding(bottom = 24.dp) ) { - if (isLoading) { - CircularProgressIndicator( - color = Color.White, - modifier = Modifier.size(24.dp), - strokeWidth = 2.dp - ) - } else { + // 错误提示 + error?.let { Text( - text = "确认开启", - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold + text = it, + color = ErrorRed, + fontSize = 14.sp, + modifier = Modifier.padding(bottom = 16.dp) ) } + + // 确认按钮 + Button( + onClick = { + val initialWeight = initialWeightText.toDoubleOrNull() + val targetWeight = targetWeightText.toDoubleOrNull() + + if (initialWeight != null && initialWeight > 0 && + targetWeight != null && targetWeight > 0 && + startDate != null && endDate != null) { + onCreateEpoch( + name.ifEmpty { "新纪元" }, + initialWeight, + targetWeight, + startDate!!.format(dateFormatter), + endDate!!.format(dateFormatter) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + enabled = { + val initialWeight = initialWeightText.toDoubleOrNull() + val targetWeight = targetWeightText.toDoubleOrNull() + initialWeight != null && initialWeight > 0 && + targetWeight != null && targetWeight > 0 && + startDate != null && endDate != null && !isLoading + }(), + shape = RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Slate900, + disabledContainerColor = Slate300 + ) + ) { + if (isLoading) { + CircularProgressIndicator( + color = Color.White, + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp + ) + } else { + Text( + text = "确认开启", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold + ) + } + } } } diff --git a/android/app/src/main/java/com/healthflow/app/ui/viewmodel/EpochViewModel.kt b/android/app/src/main/java/com/healthflow/app/ui/viewmodel/EpochViewModel.kt index 7c748b3..4eafa17 100644 --- a/android/app/src/main/java/com/healthflow/app/ui/viewmodel/EpochViewModel.kt +++ b/android/app/src/main/java/com/healthflow/app/ui/viewmodel/EpochViewModel.kt @@ -44,6 +44,9 @@ class EpochViewModel : ViewModel() { private val _profileStats = MutableStateFlow(ProfileStats()) val profileStats: StateFlow = _profileStats + private val _latestWeight = MutableStateFlow(null) + val latestWeight: StateFlow = _latestWeight + private val _exerciseDays = MutableStateFlow(0) private val _isLoading = MutableStateFlow(false) @@ -90,6 +93,17 @@ class EpochViewModel : ViewModel() { } } + // 获取最新体重记录 + try { + val historyResponse = api.getWeightHistory(1) + if (historyResponse.isSuccessful) { + val history = historyResponse.body() + _latestWeight.value = history?.firstOrNull()?.weight + } + } catch (e: Exception) { + // 忽略获取体重记录失败 + } + // 计算个人统计 calculateProfileStats() } catch (e: Exception) { @@ -176,6 +190,8 @@ class EpochViewModel : ViewModel() { fun createEpoch( name: String, + initialWeight: Double, + targetWeight: Double, startDate: String, endDate: String, onSuccess: () -> Unit @@ -184,17 +200,10 @@ class EpochViewModel : ViewModel() { _isLoading.value = true _error.value = null try { - // 获取最新体重作为初始体重 - val latestWeight = _epochDetail.value?.currentWeight - ?: _epochList.value.firstOrNull()?.let { epoch -> - api.getEpochDetail(epoch.id).body()?.currentWeight - } - ?: 80.0 // 默认值 - val request = CreateEpochRequest( name = name, - initialWeight = latestWeight, - targetWeight = latestWeight - 10, // 默认目标减10kg + initialWeight = initialWeight, + targetWeight = targetWeight, startDate = startDate, endDate = endDate )