feat:所有功能与限制均完成
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
||||
applicationId = "com.healthflow.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 102
|
||||
versionName = "3.0.2"
|
||||
versionCode = 106
|
||||
versionName = "3.0.6"
|
||||
|
||||
buildConfigField("String", "API_BASE_URL", "\"https://health.amos.us.kg/api/\"")
|
||||
buildConfigField("int", "VERSION_CODE", "102")
|
||||
buildConfigField("int", "VERSION_CODE", "106")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -55,6 +55,7 @@ fun MainNavigation(
|
||||
val epochList by epochViewModel.epochList.collectAsState()
|
||||
val activeEpoch by epochViewModel.activeEpoch.collectAsState()
|
||||
val epochDetail by epochViewModel.epochDetail.collectAsState()
|
||||
val epochDetails by epochViewModel.epochDetails.collectAsState()
|
||||
val weeklyPlans by epochViewModel.weeklyPlans.collectAsState()
|
||||
val currentWeekPlan by epochViewModel.currentWeekPlan.collectAsState()
|
||||
val selectedPlan by epochViewModel.selectedPlan.collectAsState()
|
||||
@@ -82,6 +83,7 @@ fun MainNavigation(
|
||||
composable(Tab.Epoch.route) {
|
||||
EpochScreen(
|
||||
epochs = epochList,
|
||||
epochDetails = epochDetails,
|
||||
isLoading = isLoading,
|
||||
onEpochClick = { epoch ->
|
||||
epochViewModel.setActiveEpoch(epoch)
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
package com.healthflow.app.ui.screen
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.healthflow.app.data.model.EpochDetail
|
||||
@@ -32,7 +26,6 @@ import com.healthflow.app.data.model.WeightEpoch
|
||||
import com.healthflow.app.ui.theme.*
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun EpochDetailScreen(
|
||||
@@ -71,6 +64,7 @@ fun EpochDetailScreen(
|
||||
}
|
||||
|
||||
val actualLoss = epochDetail?.actualChange ?: 0.0
|
||||
val targetLoss = epoch.initialWeight - epoch.targetWeight
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -135,25 +129,111 @@ fun EpochDetailScreen(
|
||||
color = Slate500
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 统计卡片 - 2x2 网格
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
StatItem(
|
||||
value = formatWeight(epoch.initialWeight),
|
||||
label = "初始"
|
||||
)
|
||||
StatItem(
|
||||
value = formatWeight(epoch.targetWeight),
|
||||
label = "目标"
|
||||
)
|
||||
StatItem(
|
||||
value = if (actualLoss >= 0) "-${formatWeight(actualLoss)}" else "+${formatWeight(-actualLoss)}",
|
||||
label = "已减",
|
||||
valueColor = if (actualLoss >= 0) Brand500 else ErrorRed
|
||||
)
|
||||
// 目标减重
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Slate50
|
||||
) {
|
||||
Column(modifier = Modifier.padding(14.dp)) {
|
||||
Text(
|
||||
text = "目标减重",
|
||||
fontSize = 11.sp,
|
||||
color = Slate400,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${formatWeight(targetLoss)} kg",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Slate900
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 已减
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = if (actualLoss >= 0) Brand500.copy(alpha = 0.08f) else ErrorRed.copy(alpha = 0.08f)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(14.dp)) {
|
||||
Text(
|
||||
text = "已减",
|
||||
fontSize = 11.sp,
|
||||
color = Slate400,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${formatWeight(actualLoss)} kg",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (actualLoss >= 0) Brand500 else ErrorRed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 初始体重
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Slate50
|
||||
) {
|
||||
Column(modifier = Modifier.padding(14.dp)) {
|
||||
Text(
|
||||
text = "初始",
|
||||
fontSize = 11.sp,
|
||||
color = Slate400,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${formatWeight(epoch.initialWeight)} kg",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Slate700
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 目标体重
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Slate50
|
||||
) {
|
||||
Column(modifier = Modifier.padding(14.dp)) {
|
||||
Text(
|
||||
text = "目标",
|
||||
fontSize = 11.sp,
|
||||
color = Slate400,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${formatWeight(epoch.targetWeight)} kg",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Slate700
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
@@ -220,11 +300,6 @@ private fun EditEpochDialog(
|
||||
onConfirm: (String?, Double?, Double?, String?, String?) -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf(epoch.name) }
|
||||
var initialWeight by remember { mutableStateOf(epoch.initialWeight.toString()) }
|
||||
var targetWeight by remember { mutableStateOf(epoch.targetWeight.toString()) }
|
||||
var startDate by remember { mutableStateOf(epoch.startDate.take(10)) }
|
||||
var endDate by remember { mutableStateOf(epoch.endDate.take(10)) }
|
||||
val context = LocalContext.current
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -234,121 +309,24 @@ private fun EditEpochDialog(
|
||||
Text("编辑纪元", fontWeight = FontWeight.SemiBold)
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("纪元名称") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Brand500,
|
||||
focusedLabelColor = Brand500
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("纪元名称") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Brand500,
|
||||
focusedLabelColor = Brand500
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = initialWeight,
|
||||
onValueChange = { initialWeight = it },
|
||||
label = { Text("初始体重") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Brand500,
|
||||
focusedLabelColor = Brand500
|
||||
)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = targetWeight,
|
||||
onValueChange = { targetWeight = it },
|
||||
label = { Text("目标体重") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Brand500,
|
||||
focusedLabelColor = Brand500
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
val parts = startDate.split("-")
|
||||
val cal = Calendar.getInstance()
|
||||
if (parts.size == 3) {
|
||||
cal.set(parts[0].toInt(), parts[1].toInt() - 1, parts[2].toInt())
|
||||
}
|
||||
DatePickerDialog(context, { _, y, m, d ->
|
||||
startDate = String.format("%04d-%02d-%02d", y, m + 1, d)
|
||||
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show()
|
||||
}
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = startDate,
|
||||
onValueChange = {},
|
||||
label = { Text("开始日期") },
|
||||
readOnly = true,
|
||||
enabled = false,
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledBorderColor = Slate300,
|
||||
disabledTextColor = Slate900,
|
||||
disabledLabelColor = Slate500
|
||||
)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable {
|
||||
val parts = endDate.split("-")
|
||||
val cal = Calendar.getInstance()
|
||||
if (parts.size == 3) {
|
||||
cal.set(parts[0].toInt(), parts[1].toInt() - 1, parts[2].toInt())
|
||||
}
|
||||
DatePickerDialog(context, { _, y, m, d ->
|
||||
endDate = String.format("%04d-%02d-%02d", y, m + 1, d)
|
||||
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show()
|
||||
}
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = endDate,
|
||||
onValueChange = {},
|
||||
label = { Text("结束日期") },
|
||||
readOnly = true,
|
||||
enabled = false,
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledBorderColor = Slate300,
|
||||
disabledTextColor = Slate900,
|
||||
disabledLabelColor = Slate500
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm(
|
||||
name.takeIf { it != epoch.name },
|
||||
initialWeight.toDoubleOrNull()?.takeIf { it != epoch.initialWeight },
|
||||
targetWeight.toDoubleOrNull()?.takeIf { it != epoch.targetWeight },
|
||||
startDate.takeIf { it != epoch.startDate.take(10) },
|
||||
endDate.takeIf { it != epoch.endDate.take(10) }
|
||||
null, null, null, null
|
||||
)
|
||||
}) {
|
||||
Text("保存", color = Brand500, fontWeight = FontWeight.SemiBold)
|
||||
@@ -362,28 +340,6 @@ private fun EditEpochDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatItem(
|
||||
value: String,
|
||||
label: String,
|
||||
valueColor: Color = Slate900
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = valueColor
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
color = Slate500,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class WeekStatus { PAST, CURRENT, FUTURE }
|
||||
|
||||
@Composable
|
||||
@@ -473,22 +429,52 @@ private fun WeekItem(
|
||||
}
|
||||
}
|
||||
|
||||
// 印章样式 - 更醒目
|
||||
if (showStamp) {
|
||||
val stampModifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 80.dp)
|
||||
.rotate(-15f)
|
||||
.alpha(0.12f)
|
||||
|
||||
if (isQualified) {
|
||||
Column(modifier = stampModifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(Icons.Default.Check, null, tint = Brand500, modifier = Modifier.size(48.dp))
|
||||
Text("合格", fontSize = 16.sp, fontWeight = FontWeight.ExtraBold, letterSpacing = 1.sp, color = Brand500)
|
||||
}
|
||||
} else if (isFailed) {
|
||||
Column(modifier = stampModifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(Icons.Default.Close, null, tint = ErrorRed, modifier = Modifier.size(48.dp))
|
||||
Text("不合格", fontSize = 14.sp, fontWeight = FontWeight.ExtraBold, letterSpacing = 1.sp, color = ErrorRed)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 70.dp)
|
||||
.rotate(-12f)
|
||||
) {
|
||||
if (isQualified) {
|
||||
// 合格印章 - 绿色边框 + 文字
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = Brand500.copy(alpha = 0.6f),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "合格",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
letterSpacing = 2.sp,
|
||||
color = Brand500.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
} else if (isFailed) {
|
||||
// 不合格印章 - 红色边框 + 文字
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = ErrorRed.copy(alpha = 0.6f),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "不合格",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
letterSpacing = 1.sp,
|
||||
color = ErrorRed.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.healthflow.app.ui.screen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -9,19 +10,17 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.healthflow.app.data.model.EpochDetail
|
||||
import com.healthflow.app.data.model.WeightEpoch
|
||||
import com.healthflow.app.ui.theme.*
|
||||
import java.time.LocalDate
|
||||
@@ -31,6 +30,7 @@ import java.time.temporal.ChronoUnit
|
||||
@Composable
|
||||
fun EpochScreen(
|
||||
epochs: List<WeightEpoch>,
|
||||
epochDetails: Map<Long, EpochDetail>,
|
||||
isLoading: Boolean,
|
||||
onEpochClick: (WeightEpoch) -> Unit,
|
||||
onCreateNew: () -> Unit
|
||||
@@ -104,12 +104,13 @@ fun EpochScreen(
|
||||
}
|
||||
else -> {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(bottom = 100.dp)
|
||||
) {
|
||||
items(epochs, key = { it.id }) { epoch ->
|
||||
EpochCard(
|
||||
epoch = epoch,
|
||||
detail = epochDetails[epoch.id],
|
||||
onClick = { onEpochClick(epoch) }
|
||||
)
|
||||
}
|
||||
@@ -142,6 +143,7 @@ fun EpochScreen(
|
||||
@Composable
|
||||
private fun EpochCard(
|
||||
epoch: WeightEpoch,
|
||||
detail: EpochDetail?,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val isActive = epoch.isActive
|
||||
@@ -159,6 +161,23 @@ private fun EpochCard(
|
||||
} catch (e: Exception) { 0f }
|
||||
}
|
||||
|
||||
// 计算剩余天数
|
||||
val daysRemaining = remember(epoch) {
|
||||
try {
|
||||
val end = LocalDate.parse(epoch.endDate.take(10))
|
||||
val now = LocalDate.now()
|
||||
ChronoUnit.DAYS.between(now, end).toInt().coerceAtLeast(0)
|
||||
} catch (e: Exception) { 0 }
|
||||
}
|
||||
|
||||
// 计算目标减重
|
||||
val targetLoss = remember(epoch) {
|
||||
epoch.initialWeight - epoch.targetWeight
|
||||
}
|
||||
|
||||
// 已减重 - 优先使用 detail 中的数据
|
||||
val actualLoss = detail?.actualChange ?: 0.0
|
||||
|
||||
// 格式化日期
|
||||
val dateRange = remember(epoch) {
|
||||
try {
|
||||
@@ -184,42 +203,34 @@ private fun EpochCard(
|
||||
brush = androidx.compose.ui.graphics.SolidColor(Slate200.copy(alpha = 0.8f))
|
||||
)
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
// 左侧绿色指示条
|
||||
Row(modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)) {
|
||||
// 左侧指示条
|
||||
if (isActive) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(4.dp)
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 24.dp)
|
||||
.background(Brand500, RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp))
|
||||
.background(Brand500, RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp))
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
// 已完成印章
|
||||
if (isCompleted) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 10.dp)
|
||||
.rotate(-15f)
|
||||
.alpha(0.12f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 12.dp, end = 12.dp)
|
||||
.rotate(-12f)
|
||||
.border(2.dp, Brand500.copy(alpha = 0.5f), RoundedCornerShape(4.dp))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Brand500,
|
||||
modifier = Modifier.size(80.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height((-5).dp))
|
||||
Text(
|
||||
text = "合格",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
letterSpacing = 2.sp,
|
||||
color = Brand500
|
||||
text = "已完成",
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
letterSpacing = 1.sp,
|
||||
color = Brand500.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -227,47 +238,48 @@ private fun EpochCard(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// 第一行:标题
|
||||
Text(
|
||||
text = epoch.name.ifEmpty { "未命名纪元" },
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = (-0.3).sp,
|
||||
color = Slate900
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// 第二行:日期 + 剩余天数
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = epoch.name.ifEmpty { "未命名纪元" },
|
||||
fontSize = 19.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = (-0.3).sp,
|
||||
color = Slate900
|
||||
text = dateRange,
|
||||
fontSize = 13.sp,
|
||||
color = Slate400
|
||||
)
|
||||
|
||||
if (isActive && !isCompleted) {
|
||||
Text(
|
||||
text = "进行中",
|
||||
fontSize = 14.sp,
|
||||
text = "剩余 $daysRemaining 天",
|
||||
fontSize = 12.sp,
|
||||
color = Brand500,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = dateRange,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Slate500
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 进度条
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(6.dp)
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Slate100)
|
||||
) {
|
||||
@@ -278,8 +290,52 @@ private fun EpochCard(
|
||||
.background(Slate900, RoundedCornerShape(10.dp))
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// 底部:目标 + 已减
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "目标 ",
|
||||
fontSize = 13.sp,
|
||||
color = Slate400
|
||||
)
|
||||
Text(
|
||||
text = "${formatWeight(targetLoss)} kg",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Slate700
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "已减 ",
|
||||
fontSize = 13.sp,
|
||||
color = Slate400
|
||||
)
|
||||
Text(
|
||||
text = "${formatWeight(actualLoss)} kg",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (actualLoss >= 0) Brand500 else ErrorRed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatWeight(weight: Double): String {
|
||||
return if (weight == weight.toLong().toDouble()) {
|
||||
weight.toLong().toString()
|
||||
} else {
|
||||
String.format("%.1f", kotlin.math.abs(weight))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -32,6 +31,8 @@ fun ProfileScreen(
|
||||
onLogout: () -> Unit,
|
||||
onCheckUpdate: () -> Unit
|
||||
) {
|
||||
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -41,7 +42,7 @@ fun ProfileScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 顶部:标题 + 操作按钮
|
||||
// 顶部:标题 + 更新按钮
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -55,33 +56,17 @@ fun ProfileScreen(
|
||||
color = Slate900
|
||||
)
|
||||
|
||||
// 右侧操作按钮
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
// 检查更新按钮
|
||||
IconButton(
|
||||
onClick = onCheckUpdate,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Refresh,
|
||||
contentDescription = "检查更新",
|
||||
tint = Slate500,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// 退出登录按钮
|
||||
IconButton(
|
||||
onClick = onLogout,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.ExitToApp,
|
||||
contentDescription = "退出登录",
|
||||
tint = Slate500,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
// 检查更新按钮
|
||||
IconButton(
|
||||
onClick = onCheckUpdate,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Refresh,
|
||||
contentDescription = "检查更新",
|
||||
tint = Slate500,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +151,51 @@ fun ProfileScreen(
|
||||
|
||||
// 年度进度卡片
|
||||
YearProgressCard(daysRemaining = stats.daysRemaining)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// 退出登录按钮 - 放在底部,文字按钮样式
|
||||
TextButton(
|
||||
onClick = { showLogoutDialog = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 120.dp),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = Slate400)
|
||||
) {
|
||||
Text(
|
||||
text = "退出登录",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 退出确认弹窗
|
||||
if (showLogoutDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLogoutDialog = false },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
containerColor = Color.White,
|
||||
title = {
|
||||
Text("退出登录", fontWeight = FontWeight.SemiBold)
|
||||
},
|
||||
text = {
|
||||
Text("确定要退出当前账号吗?")
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showLogoutDialog = false
|
||||
onLogout()
|
||||
}) {
|
||||
Text("退出", color = ErrorRed, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showLogoutDialog = false }) {
|
||||
Text("取消", color = Slate500)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ import androidx.compose.ui.window.Dialog
|
||||
import com.healthflow.app.data.model.WeeklyPlanDetail
|
||||
import com.healthflow.app.data.model.WeightEpoch
|
||||
import com.healthflow.app.ui.theme.*
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
// 周计划状态枚举
|
||||
@@ -82,6 +85,12 @@ fun WeekPlanDetailScreen(
|
||||
// 是否允许操作(只有当前周可以操作)
|
||||
val canOperate = weekStatus == PlanWeekStatus.CURRENT
|
||||
|
||||
// 是否可以记录体重(周日 22:00 后才能记录)
|
||||
val now = LocalDateTime.now()
|
||||
val canRecordWeight = canOperate &&
|
||||
now.dayOfWeek == DayOfWeek.SUNDAY &&
|
||||
now.toLocalTime() >= LocalTime.of(22, 0)
|
||||
|
||||
// 是否可以修改目标(目标未设置或初始体重可编辑)
|
||||
val canEditTarget = canOperate && (plan.targetWeight == null || planDetail.initialWeightEditable)
|
||||
|
||||
@@ -174,7 +183,7 @@ fun WeekPlanDetailScreen(
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Button(
|
||||
onClick = { showWeightDialog = true },
|
||||
enabled = canOperate,
|
||||
enabled = canRecordWeight,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
@@ -185,7 +194,7 @@ fun WeekPlanDetailScreen(
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "添加体重记录",
|
||||
text = if (canOperate && !canRecordWeight) "周日 22:00 后可记录" else "添加体重记录",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -28,6 +28,9 @@ class EpochViewModel : ViewModel() {
|
||||
private val _epochDetail = MutableStateFlow<EpochDetail?>(null)
|
||||
val epochDetail: StateFlow<EpochDetail?> = _epochDetail
|
||||
|
||||
private val _epochDetails = MutableStateFlow<Map<Long, EpochDetail>>(emptyMap())
|
||||
val epochDetails: StateFlow<Map<Long, EpochDetail>> = _epochDetails
|
||||
|
||||
private val _weeklyPlans = MutableStateFlow<List<WeeklyPlanDetail>>(emptyList())
|
||||
val weeklyPlans: StateFlow<List<WeeklyPlanDetail>> = _weeklyPlans
|
||||
|
||||
@@ -53,7 +56,24 @@ class EpochViewModel : ViewModel() {
|
||||
// 加载纪元列表
|
||||
val listResponse = api.getEpochList()
|
||||
if (listResponse.isSuccessful) {
|
||||
_epochList.value = listResponse.body() ?: emptyList()
|
||||
val epochs = listResponse.body() ?: emptyList()
|
||||
_epochList.value = epochs
|
||||
|
||||
// 加载所有纪元的详情
|
||||
val detailsMap = mutableMapOf<Long, EpochDetail>()
|
||||
epochs.forEach { epoch ->
|
||||
try {
|
||||
val detailResponse = api.getEpochDetail(epoch.id)
|
||||
if (detailResponse.isSuccessful) {
|
||||
detailResponse.body()?.let { detail ->
|
||||
detailsMap[epoch.id] = detail
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 忽略单个纪元详情加载失败
|
||||
}
|
||||
}
|
||||
_epochDetails.value = detailsMap
|
||||
}
|
||||
|
||||
// 加载活跃纪元
|
||||
|
||||
Reference in New Issue
Block a user