创建纪元页面支持设置初始体重和减重目标
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
|||||||
applicationId = "com.healthflow.app"
|
applicationId = "com.healthflow.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 116
|
versionCode = 118
|
||||||
versionName = "3.1.6"
|
versionName = "3.1.8"
|
||||||
|
|
||||||
buildConfigField("String", "API_BASE_URL", "\"https://health.amos.us.kg/api/\"")
|
buildConfigField("String", "API_BASE_URL", "\"https://health.amos.us.kg/api/\"")
|
||||||
buildConfigField("int", "VERSION_CODE", "116")
|
buildConfigField("int", "VERSION_CODE", "118")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -167,12 +167,14 @@ fun MainNavigation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable(Routes.CREATE_EPOCH) {
|
composable(Routes.CREATE_EPOCH) {
|
||||||
|
val latestWeight by epochViewModel.latestWeight.collectAsState()
|
||||||
CreateEpochScreen(
|
CreateEpochScreen(
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
error = error,
|
error = error,
|
||||||
|
latestWeight = latestWeight,
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onCreateEpoch = { name, startDate, endDate ->
|
onCreateEpoch = { name, initialWeight, targetWeight, startDate, endDate ->
|
||||||
epochViewModel.createEpoch(name, startDate, endDate) {
|
epochViewModel.createEpoch(name, initialWeight, targetWeight, startDate, endDate) {
|
||||||
epochViewModel.loadAll()
|
epochViewModel.loadAll()
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package com.healthflow.app.ui.screen
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -23,10 +25,14 @@ import java.time.format.DateTimeFormatter
|
|||||||
fun CreateEpochScreen(
|
fun CreateEpochScreen(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
error: String?,
|
error: String?,
|
||||||
|
latestWeight: Double?,
|
||||||
onBack: () -> Unit,
|
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 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 startDate by remember { mutableStateOf<LocalDate?>(null) }
|
||||||
var endDate by remember { mutableStateOf<LocalDate?>(null) }
|
var endDate by remember { mutableStateOf<LocalDate?>(null) }
|
||||||
|
|
||||||
@@ -34,47 +40,85 @@ fun CreateEpochScreen(
|
|||||||
var showEndDatePicker by remember { mutableStateOf(false) }
|
var showEndDatePicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.White)
|
.background(Color.White)
|
||||||
.statusBarsPadding()
|
.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(
|
Column(
|
||||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
|
||||||
contentDescription = "返回",
|
|
||||||
tint = Slate900,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(x = (-8).dp)
|
.weight(1f)
|
||||||
.size(32.dp)
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onBack)
|
.padding(horizontal = 24.dp)
|
||||||
)
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(32.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))
|
|
||||||
|
|
||||||
// 纪元名称
|
// 纪元名称
|
||||||
Text(
|
Text(
|
||||||
@@ -103,6 +147,106 @@ fun CreateEpochScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
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(
|
||||||
text = "起始日期",
|
text = "起始日期",
|
||||||
@@ -133,52 +277,75 @@ fun CreateEpochScreen(
|
|||||||
onClick = { showEndDatePicker = true }
|
onClick = { showEndDatePicker = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 错误提示
|
// 底部留白,避免被按钮遮挡
|
||||||
error?.let {
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
color = ErrorRed,
|
|
||||||
fontSize = 14.sp
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
// 底部按钮区域(固定)
|
||||||
|
Column(
|
||||||
// 确认按钮
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (startDate != null && endDate != null) {
|
|
||||||
onCreateEpoch(
|
|
||||||
name.ifEmpty { "新纪元" },
|
|
||||||
startDate!!.format(dateFormatter),
|
|
||||||
endDate!!.format(dateFormatter)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp),
|
.padding(horizontal = 24.dp)
|
||||||
enabled = startDate != null && endDate != null && !isLoading,
|
.padding(bottom = 24.dp)
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Slate900,
|
|
||||||
disabledContainerColor = Slate300
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
if (isLoading) {
|
// 错误提示
|
||||||
CircularProgressIndicator(
|
error?.let {
|
||||||
color = Color.White,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
strokeWidth = 2.dp
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Text(
|
Text(
|
||||||
text = "确认开启",
|
text = it,
|
||||||
fontSize = 16.sp,
|
color = ErrorRed,
|
||||||
fontWeight = FontWeight.SemiBold
|
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())
|
private val _profileStats = MutableStateFlow(ProfileStats())
|
||||||
val profileStats: StateFlow<ProfileStats> = _profileStats
|
val profileStats: StateFlow<ProfileStats> = _profileStats
|
||||||
|
|
||||||
|
private val _latestWeight = MutableStateFlow<Double?>(null)
|
||||||
|
val latestWeight: StateFlow<Double?> = _latestWeight
|
||||||
|
|
||||||
private val _exerciseDays = MutableStateFlow(0)
|
private val _exerciseDays = MutableStateFlow(0)
|
||||||
|
|
||||||
private val _isLoading = MutableStateFlow(false)
|
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()
|
calculateProfileStats()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -176,6 +190,8 @@ class EpochViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun createEpoch(
|
fun createEpoch(
|
||||||
name: String,
|
name: String,
|
||||||
|
initialWeight: Double,
|
||||||
|
targetWeight: Double,
|
||||||
startDate: String,
|
startDate: String,
|
||||||
endDate: String,
|
endDate: String,
|
||||||
onSuccess: () -> Unit
|
onSuccess: () -> Unit
|
||||||
@@ -184,17 +200,10 @@ class EpochViewModel : ViewModel() {
|
|||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
_error.value = null
|
_error.value = null
|
||||||
try {
|
try {
|
||||||
// 获取最新体重作为初始体重
|
|
||||||
val latestWeight = _epochDetail.value?.currentWeight
|
|
||||||
?: _epochList.value.firstOrNull()?.let { epoch ->
|
|
||||||
api.getEpochDetail(epoch.id).body()?.currentWeight
|
|
||||||
}
|
|
||||||
?: 80.0 // 默认值
|
|
||||||
|
|
||||||
val request = CreateEpochRequest(
|
val request = CreateEpochRequest(
|
||||||
name = name,
|
name = name,
|
||||||
initialWeight = latestWeight,
|
initialWeight = initialWeight,
|
||||||
targetWeight = latestWeight - 10, // 默认目标减10kg
|
targetWeight = targetWeight,
|
||||||
startDate = startDate,
|
startDate = startDate,
|
||||||
endDate = endDate
|
endDate = endDate
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user