创建纪元页面支持设置初始体重和减重目标
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user