创建纪元页面支持设置初始体重和减重目标

This commit is contained in:
amos
2026-03-09 10:02:34 +08:00
parent 898aa65f44
commit 8abf905ef5
4 changed files with 263 additions and 85 deletions

View File

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

View File

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

View File

@@ -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<LocalDate?>(null) }
var endDate by remember { mutableStateOf<LocalDate?>(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
)
}
}
}
}

View File

@@ -44,6 +44,9 @@ class EpochViewModel : ViewModel() {
private val _profileStats = MutableStateFlow(ProfileStats())
val profileStats: StateFlow<ProfileStats> = _profileStats
private val _latestWeight = MutableStateFlow<Double?>(null)
val latestWeight: StateFlow<Double?> = _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
)