feat:所有功能与限制均完成

This commit is contained in:
amos
2025-12-29 17:58:03 +08:00
parent aa20632b7a
commit 3605210681
7 changed files with 358 additions and 255 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
// 加载活跃纪元