feat:周计划展示逻辑优化
This commit is contained in:
4
android/.kotlin/errors/errors-1767000134829.log
Normal file
4
android/.kotlin/errors/errors-1767000134829.log
Normal file
@@ -0,0 +1,4 @@
|
||||
kotlin version: 2.0.21
|
||||
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||
1. Kotlin compile daemon is ready
|
||||
|
||||
@@ -13,11 +13,11 @@ android {
|
||||
applicationId = "com.healthflow.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 45
|
||||
versionName = "2.2.2"
|
||||
versionCode = 102
|
||||
versionName = "3.0.2"
|
||||
|
||||
buildConfigField("String", "API_BASE_URL", "\"https://health.amos.us.kg/api/\"")
|
||||
buildConfigField("int", "VERSION_CODE", "45")
|
||||
buildConfigField("int", "VERSION_CODE", "102")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -94,22 +94,52 @@ fun MainNavigation(
|
||||
}
|
||||
|
||||
composable(Tab.Plan.route) {
|
||||
PlanScreen(
|
||||
activeEpoch = activeEpoch,
|
||||
epochDetail = epochDetail,
|
||||
currentWeekPlan = currentWeekPlan,
|
||||
weeklyPlans = weeklyPlans,
|
||||
isLoading = isLoading,
|
||||
onPlanClick = { planDetail ->
|
||||
epochViewModel.selectPlan(planDetail)
|
||||
activeEpoch?.let { epoch ->
|
||||
navController.navigate(Routes.weekPlanDetail(epoch.id, planDetail.plan.id))
|
||||
// 直接显示本周计划详情
|
||||
val epoch = activeEpoch
|
||||
val plan = currentWeekPlan
|
||||
if (epoch == null) {
|
||||
// 没有活跃纪元时显示空状态
|
||||
PlanScreen(
|
||||
activeEpoch = null,
|
||||
epochDetail = null,
|
||||
currentWeekPlan = null,
|
||||
weeklyPlans = emptyList(),
|
||||
isLoading = isLoading,
|
||||
onPlanClick = {},
|
||||
onCreateEpoch = {
|
||||
navController.navigate(Routes.CREATE_EPOCH)
|
||||
}
|
||||
},
|
||||
onCreateEpoch = {
|
||||
navController.navigate(Routes.CREATE_EPOCH)
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// 有活跃纪元时直接显示本周计划详情
|
||||
WeekPlanDetailScreen(
|
||||
epoch = epoch,
|
||||
planDetail = plan,
|
||||
onBack = {
|
||||
// 计划 Tab 不需要返回,切换到纪元 Tab
|
||||
navController.navigate(Tab.Epoch.route) {
|
||||
popUpTo(navController.graph.startDestinationId) {
|
||||
inclusive = false
|
||||
}
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
onRecordWeight = { weight ->
|
||||
plan?.let { p ->
|
||||
epochViewModel.recordWeight(epoch.id, p.plan.id, weight) {
|
||||
epochViewModel.loadAll()
|
||||
}
|
||||
}
|
||||
},
|
||||
onUpdateTarget = { initialWeight, targetWeight ->
|
||||
plan?.let { p ->
|
||||
epochViewModel.updateWeeklyPlanTarget(epoch.id, p.plan.id, initialWeight, targetWeight) {
|
||||
epochViewModel.loadAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composable(Tab.Profile.route) {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -15,7 +16,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.healthflow.app.data.model.*
|
||||
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
|
||||
|
||||
@Composable
|
||||
@@ -33,6 +37,30 @@ fun PlanScreen(
|
||||
return
|
||||
}
|
||||
|
||||
// 判断是否解锁下一周
|
||||
// 周日 22:00 后 或 周一全天 都可以看到下一周计划(方便提前规划)
|
||||
val now = LocalDateTime.now()
|
||||
val isNextWeekUnlocked = remember(now) {
|
||||
val dayOfWeek = now.dayOfWeek
|
||||
val time = now.toLocalTime()
|
||||
when {
|
||||
// 周日 22:00 后
|
||||
dayOfWeek == DayOfWeek.SUNDAY && time >= LocalTime.of(22, 0) -> true
|
||||
// 周一全天也可以看到(刚开始新的一周,可以规划)
|
||||
dayOfWeek == DayOfWeek.MONDAY -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// 找到下一周计划
|
||||
val nextWeekPlan = remember(weeklyPlans, currentWeekPlan) {
|
||||
if (currentWeekPlan == null) return@remember null
|
||||
val currentIndex = weeklyPlans.indexOfFirst { it.plan.id == currentWeekPlan.plan.id }
|
||||
if (currentIndex >= 0 && currentIndex < weeklyPlans.size - 1) {
|
||||
weeklyPlans[currentIndex + 1]
|
||||
} else null
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -61,42 +89,71 @@ fun PlanScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// 正在进行中
|
||||
Text(
|
||||
text = "正在进行中",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Slate500,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 当前周计划卡片
|
||||
if (currentWeekPlan != null) {
|
||||
CurrentWeekCard(
|
||||
epoch = activeEpoch,
|
||||
plan = currentWeekPlan,
|
||||
onClick = { onPlanClick(currentWeekPlan) }
|
||||
)
|
||||
} else {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp,
|
||||
border = ButtonDefaults.outlinedButtonBorder.copy(
|
||||
width = 1.dp,
|
||||
brush = androidx.compose.ui.graphics.SolidColor(Slate200.copy(alpha = 0.8f))
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(bottom = 100.dp)
|
||||
) {
|
||||
// 正在进行中
|
||||
item {
|
||||
Text(
|
||||
text = "正在进行中",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Slate500,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "本周暂无计划", color = Slate500, fontSize = 14.sp)
|
||||
}
|
||||
|
||||
// 当前周计划卡片
|
||||
item {
|
||||
if (currentWeekPlan != null) {
|
||||
CurrentWeekCard(
|
||||
epoch = activeEpoch,
|
||||
plan = currentWeekPlan,
|
||||
onClick = { onPlanClick(currentWeekPlan) }
|
||||
)
|
||||
} else {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp,
|
||||
border = ButtonDefaults.outlinedButtonBorder.copy(
|
||||
width = 1.dp,
|
||||
brush = androidx.compose.ui.graphics.SolidColor(Slate200.copy(alpha = 0.8f))
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "本周暂无计划", color = Slate500, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下一周计划(周日 22:00 后解锁)
|
||||
if (isNextWeekUnlocked && nextWeekPlan != null) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "即将开始",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Slate500,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
NextWeekCard(
|
||||
epoch = activeEpoch,
|
||||
plan = nextWeekPlan,
|
||||
onClick = { onPlanClick(nextWeekPlan) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,6 +334,89 @@ private fun CurrentWeekCard(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NextWeekCard(
|
||||
epoch: WeightEpoch,
|
||||
plan: WeeklyPlanDetail,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
// 周序号 - 基于天数计算
|
||||
val weekIndex = remember(plan, epoch) {
|
||||
try {
|
||||
val epochStart = LocalDate.parse(epoch.startDate.take(10))
|
||||
val planStart = LocalDate.parse(plan.plan.startDate.take(10))
|
||||
val daysBetween = ChronoUnit.DAYS.between(epochStart, planStart)
|
||||
(daysBetween / 7 + 1).toInt()
|
||||
} catch (e: Exception) { plan.plan.week }
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable { onClick() },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White,
|
||||
shadowElevation = 2.dp,
|
||||
border = ButtonDefaults.outlinedButtonBorder.copy(
|
||||
width = 1.dp,
|
||||
brush = androidx.compose.ui.graphics.SolidColor(Slate200.copy(alpha = 0.8f))
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
) {
|
||||
// 左侧橙色指示条(即将开始)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(4.dp)
|
||||
.fillMaxHeight()
|
||||
.background(Color(0xFFF59E0B), RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp))
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "${epoch.name} - 第 $weekIndex 周",
|
||||
fontSize = 19.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = (-0.3).sp,
|
||||
color = Slate900
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "即将开始",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFFF59E0B),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 目标信息
|
||||
val targetText = plan.plan.targetWeight?.let { "目标: ${formatWeight(it)} kg" } ?: "目标待设定"
|
||||
Text(
|
||||
text = targetText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Slate500
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatWeight(weight: Double): String {
|
||||
return if (weight == weight.toLong().toDouble()) {
|
||||
weight.toLong().toString()
|
||||
|
||||
@@ -81,6 +81,9 @@ fun WeekPlanDetailScreen(
|
||||
|
||||
// 是否允许操作(只有当前周可以操作)
|
||||
val canOperate = weekStatus == PlanWeekStatus.CURRENT
|
||||
|
||||
// 是否可以修改目标(目标未设置或初始体重可编辑)
|
||||
val canEditTarget = canOperate && (plan.targetWeight == null || planDetail.initialWeightEditable)
|
||||
|
||||
// 状态描述文字
|
||||
val statusText = when (weekStatus) {
|
||||
@@ -192,14 +195,14 @@ fun WeekPlanDetailScreen(
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { showTargetDialog = true },
|
||||
enabled = canOperate,
|
||||
enabled = canEditTarget,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
border = ButtonDefaults.outlinedButtonBorder.copy(
|
||||
width = 1.dp,
|
||||
brush = androidx.compose.ui.graphics.SolidColor(if (canOperate) Slate900 else Slate300)
|
||||
brush = androidx.compose.ui.graphics.SolidColor(if (canEditTarget) Slate900 else Slate300)
|
||||
),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = Slate900,
|
||||
@@ -207,7 +210,7 @@ fun WeekPlanDetailScreen(
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "修改周目标",
|
||||
text = if (plan.targetWeight != null && !planDetail.initialWeightEditable) "目标已锁定" else "设置周目标",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -408,6 +411,19 @@ private fun TargetEditDialog(
|
||||
) {
|
||||
var initialText by remember { mutableStateOf(initialWeight?.let { formatWeight(it) } ?: "") }
|
||||
var targetText by remember { mutableStateOf(targetWeight?.let { formatWeight(it) } ?: "") }
|
||||
var lossText by remember { mutableStateOf("") }
|
||||
|
||||
// 目标体重是否已设置(已设置则不可修改)
|
||||
val targetEditable = targetWeight == null
|
||||
|
||||
// 当输入目标减重时,自动计算目标体重
|
||||
LaunchedEffect(lossText, initialText) {
|
||||
val loss = lossText.toDoubleOrNull()
|
||||
val initial = initialText.toDoubleOrNull()
|
||||
if (loss != null && initial != null && targetEditable) {
|
||||
targetText = formatWeight(initial - loss)
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
@@ -467,24 +483,70 @@ private fun TargetEditDialog(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 目标减重(仅当目标体重未设置时可用)
|
||||
if (targetEditable) {
|
||||
OutlinedTextField(
|
||||
value = lossText,
|
||||
onValueChange = { newValue ->
|
||||
if (isValidWeightInput(newValue)) {
|
||||
lossText = newValue
|
||||
}
|
||||
},
|
||||
label = { Text("目标减重 (kg)", color = Slate500) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Slate900,
|
||||
unfocusedBorderColor = Slate200,
|
||||
focusedLabelColor = Slate900,
|
||||
cursorColor = Slate900
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
),
|
||||
supportingText = {
|
||||
Text(
|
||||
text = "输入后自动计算目标体重",
|
||||
fontSize = 12.sp,
|
||||
color = Slate400
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// 周目标体重
|
||||
OutlinedTextField(
|
||||
value = targetText,
|
||||
onValueChange = { newValue ->
|
||||
if (isValidWeightInput(newValue)) {
|
||||
if (targetEditable && isValidWeightInput(newValue)) {
|
||||
targetText = newValue
|
||||
lossText = "" // 清空目标减重
|
||||
}
|
||||
},
|
||||
label = { Text("周目标体重 (kg)", color = Slate500) },
|
||||
label = {
|
||||
Text(
|
||||
if (targetEditable) "周目标体重 (kg)" else "周目标体重 (kg) - 已锁定",
|
||||
color = Slate500
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
singleLine = true,
|
||||
enabled = targetEditable,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Slate900,
|
||||
unfocusedBorderColor = Slate200,
|
||||
focusedLabelColor = Slate900,
|
||||
cursorColor = Slate900
|
||||
cursorColor = Slate900,
|
||||
disabledBorderColor = Slate200,
|
||||
disabledTextColor = Slate600,
|
||||
disabledLabelColor = Slate400
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
fontSize = 18.sp,
|
||||
@@ -516,7 +578,10 @@ private fun TargetEditDialog(
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
onConfirm(initialText.toDoubleOrNull(), targetText.toDoubleOrNull())
|
||||
onConfirm(
|
||||
if (initialWeightEditable) initialText.toDoubleOrNull() else null,
|
||||
if (targetEditable) targetText.toDoubleOrNull() else null
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
|
||||
@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.WeekFields
|
||||
|
||||
data class ProfileStats(
|
||||
val yearLoss: Double = 0.0,
|
||||
@@ -92,12 +91,16 @@ class EpochViewModel : ViewModel() {
|
||||
val plans = plansResponse.body() ?: emptyList()
|
||||
_weeklyPlans.value = plans
|
||||
|
||||
// 找到当前周计划
|
||||
// 找到当前周计划 - 根据日期范围判断
|
||||
val now = LocalDate.now()
|
||||
val currentYear = now.year
|
||||
val currentWeek = now.get(WeekFields.ISO.weekOfWeekBasedYear())
|
||||
_currentWeekPlan.value = plans.find { plan: WeeklyPlanDetail ->
|
||||
plan.plan.year == currentYear && plan.plan.week == currentWeek
|
||||
try {
|
||||
val startDate = LocalDate.parse(plan.plan.startDate.take(10))
|
||||
val endDate = LocalDate.parse(plan.plan.endDate.take(10))
|
||||
!now.isBefore(startDate) && !now.isAfter(endDate)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -10,7 +10,7 @@ COPY . .
|
||||
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o healthflow-server .
|
||||
|
||||
FROM --platform=linux/amd64 alpine:latest
|
||||
RUN apk --no-cache add ca-certificates sqlite-libs
|
||||
RUN apk --no-cache add ca-certificates sqlite-libs sqlite
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/healthflow-server .
|
||||
|
||||
Reference in New Issue
Block a user