feat:新 UI

This commit is contained in:
amos
2025-12-22 17:36:20 +08:00
parent 12e7c15d2c
commit 2aa50886ad
7 changed files with 394 additions and 424 deletions

View File

@@ -13,11 +13,11 @@ android {
applicationId = "com.healthflow.app"
minSdk = 26
targetSdk = 35
versionCode = 30
versionName = "2.0.7"
versionCode = 31
versionName = "2.0.8"
buildConfigField("String", "API_BASE_URL", "\"https://health.amos.us.kg/api/\"")
buildConfigField("int", "VERSION_CODE", "30")
buildConfigField("int", "VERSION_CODE", "31")
}
signingConfigs {

View File

@@ -1,12 +1,16 @@
package com.healthflow.app.ui.navigation
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -16,7 +20,6 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.healthflow.app.R
import com.healthflow.app.data.model.User
import com.healthflow.app.ui.screen.*
import com.healthflow.app.ui.theme.*
@@ -57,39 +60,22 @@ fun MainNavigation(
val error by epochViewModel.error.collectAsState()
val profileStats by epochViewModel.profileStats.collectAsState()
// 初始化加载
LaunchedEffect(Unit) {
epochViewModel.loadAll()
}
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
// 判断是否显示底部导航
val showBottomNav = currentRoute in tabs.map { it.route }
Scaffold(
bottomBar = {
if (showBottomNav) {
BottomNavBar(
currentRoute = currentRoute,
onTabSelected = { tab ->
navController.navigate(tab.route) {
popUpTo(Tab.Epoch.route) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
)
}
}
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) {
NavHost(
navController = navController,
startDestination = Tab.Epoch.route,
modifier = Modifier.padding(paddingValues)
modifier = Modifier
.fillMaxSize()
.padding(bottom = if (showBottomNav) 64.dp else 0.dp)
) {
// 纪元 Tab
composable(Tab.Epoch.route) {
EpochScreen(
epochs = epochList,
@@ -104,7 +90,6 @@ fun MainNavigation(
)
}
// 计划 Tab
composable(Tab.Plan.route) {
PlanScreen(
activeEpoch = activeEpoch,
@@ -124,7 +109,6 @@ fun MainNavigation(
)
}
// 我的 Tab
composable(Tab.Profile.route) {
ProfileScreen(
user = user,
@@ -134,7 +118,6 @@ fun MainNavigation(
)
}
// 创建纪元页面
composable(Routes.CREATE_EPOCH) {
CreateEpochScreen(
isLoading = isLoading,
@@ -149,7 +132,6 @@ fun MainNavigation(
)
}
// 周计划详情页面
composable(
route = Routes.WEEK_PLAN_DETAIL,
arguments = listOf(
@@ -180,38 +162,47 @@ fun MainNavigation(
)
}
}
}
}
@Composable
private fun BottomNavBar(
currentRoute: String?,
onTabSelected: (Tab) -> Unit
) {
Surface(
color = Color.White,
shadowElevation = 8.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.height(64.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
tabs.forEach { tab ->
val selected = currentRoute == tab.route
NavBarItem(
label = tab.label,
selected = selected,
icon = when (tab) {
Tab.Epoch -> TabIcon.Circle
Tab.Plan -> TabIcon.Square
Tab.Profile -> TabIcon.Triangle
},
onClick = { onTabSelected(tab) }
)
// 底部导航栏
if (showBottomNav) {
Column(
modifier = Modifier.align(Alignment.BottomCenter)
) {
HorizontalDivider(color = Slate200, thickness = 1.dp)
Surface(
color = Color.White,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.height(64.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
tabs.forEach { tab ->
val selected = currentRoute == tab.route
NavBarItem(
label = tab.label,
selected = selected,
icon = when (tab) {
Tab.Epoch -> TabIcon.Circle
Tab.Plan -> TabIcon.Square
Tab.Profile -> TabIcon.Triangle
},
onClick = {
navController.navigate(tab.route) {
popUpTo(Tab.Epoch.route) { saveState = true }
launchSingleTop = true
restoreState = true
}
},
modifier = Modifier.weight(1f)
)
}
}
}
}
}
}
@@ -224,73 +215,62 @@ private fun NavBarItem(
label: String,
selected: Boolean,
icon: TabIcon,
onClick: () -> Unit
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val color = if (selected) Slate800 else Slate400
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(horizontal = 24.dp)
modifier = modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onClick
)
.padding(vertical = 8.dp)
) {
IconButton(onClick = onClick) {
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
when (icon) {
TabIcon.Circle -> {
Box(
modifier = Modifier
.size(24.dp)
.then(
if (selected) {
Modifier
} else {
Modifier
}
),
contentAlignment = Alignment.Center
) {
androidx.compose.foundation.Canvas(modifier = Modifier.size(20.dp)) {
drawCircle(
color = color,
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2.dp.toPx())
)
}
Canvas(modifier = Modifier.size(20.dp)) {
drawCircle(
color = color,
style = Stroke(width = 2.dp.toPx())
)
}
}
TabIcon.Square -> {
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
androidx.compose.foundation.Canvas(modifier = Modifier.size(18.dp)) {
drawRect(
color = color,
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2.dp.toPx())
)
}
Canvas(modifier = Modifier.size(18.dp)) {
drawRect(
color = color,
style = Stroke(width = 2.dp.toPx())
)
}
}
TabIcon.Triangle -> {
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
androidx.compose.foundation.Canvas(modifier = Modifier.size(20.dp)) {
val path = androidx.compose.ui.graphics.Path().apply {
moveTo(size.width / 2, 0f)
lineTo(size.width, size.height)
lineTo(0f, size.height)
close()
}
drawPath(
path = path,
color = color,
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2.dp.toPx())
)
Canvas(modifier = Modifier.size(20.dp)) {
val path = Path().apply {
moveTo(size.width / 2, 1f)
lineTo(size.width - 1f, size.height - 1f)
lineTo(1f, size.height - 1f)
close()
}
drawPath(
path = path,
color = color,
style = Stroke(width = 2.dp.toPx())
)
}
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = label,
fontSize = 12.sp,

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.ArrowBack
import androidx.compose.material.icons.filled.DateRange
@@ -14,7 +13,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.ui.theme.*
@@ -43,35 +41,38 @@ fun CreateEpochScreen(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(horizontal = 24.dp)
.statusBarsPadding()
.padding(horizontal = 20.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
// 返回按钮
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable(onClick = onBack)
modifier = Modifier
.clickable(onClick = onBack)
.padding(vertical = 8.dp)
) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "返回",
tint = Slate600,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "取消",
fontSize = 16.sp,
fontSize = 15.sp,
color = Slate600
)
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(20.dp))
// 标题
Text(
text = "开启新纪元",
fontSize = 28.sp,
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = Slate900
)
@@ -80,15 +81,15 @@ fun CreateEpochScreen(
text = "为你的健康阶段设定一个开端",
fontSize = 14.sp,
color = Slate500,
modifier = Modifier.padding(top = 4.dp)
modifier = Modifier.padding(top = 2.dp)
)
Spacer(modifier = Modifier.height(40.dp))
Spacer(modifier = Modifier.height(32.dp))
// 纪元名称
Text(
text = "纪元名称",
fontSize = 14.sp,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = Slate700
)
@@ -96,24 +97,25 @@ fun CreateEpochScreen(
OutlinedTextField(
value = name,
onValueChange = { name = it },
placeholder = { Text("例如:盛夏减脂阶段", color = Slate400) },
placeholder = { Text("例如:盛夏减脂阶段", color = Slate400, fontSize = 15.sp) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
shape = RoundedCornerShape(10.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Slate300,
unfocusedBorderColor = Slate200,
focusedContainerColor = Color.White,
unfocusedContainerColor = Color.White
),
singleLine = true
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 15.sp)
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(20.dp))
// 起始日期
Text(
text = "起始日期",
fontSize = 14.sp,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = Slate700
)
@@ -124,12 +126,12 @@ fun CreateEpochScreen(
onClick = { showStartDatePicker = true }
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(20.dp))
// 截止日期
Text(
text = "截止日期",
fontSize = 14.sp,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = Slate700
)
@@ -146,7 +148,7 @@ fun CreateEpochScreen(
Text(
text = it,
color = ErrorRed,
fontSize = 14.sp
fontSize = 13.sp
)
}
@@ -165,9 +167,9 @@ fun CreateEpochScreen(
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
.height(52.dp),
enabled = startDate != null && endDate != null && !isLoading,
shape = RoundedCornerShape(16.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Slate900,
disabledContainerColor = Slate300
@@ -176,19 +178,19 @@ fun CreateEpochScreen(
if (isLoading) {
CircularProgressIndicator(
color = Color.White,
modifier = Modifier.size(24.dp),
modifier = Modifier.size(22.dp),
strokeWidth = 2.dp
)
} else {
Text(
text = "确认开启",
fontSize = 16.sp,
fontSize = 15.sp,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(28.dp))
}
// 日期选择器
@@ -221,29 +223,34 @@ private fun DatePickerField(
placeholder: String,
onClick: () -> Unit
) {
OutlinedTextField(
value = value,
onValueChange = {},
placeholder = { Text(placeholder, color = Slate400) },
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
enabled = false,
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
disabledBorderColor = Slate200,
disabledContainerColor = Color.White,
disabledTextColor = Slate900
),
trailingIcon = {
shape = RoundedCornerShape(10.dp),
color = Color.White,
border = ButtonDefaults.outlinedButtonBorder.copy(width = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = value.ifEmpty { placeholder },
fontSize = 15.sp,
color = if (value.isEmpty()) Slate400 else Slate900
)
Icon(
Icons.Default.DateRange,
contentDescription = "选择日期",
tint = Slate400
tint = Slate400,
modifier = Modifier.size(20.dp)
)
},
singleLine = true
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)

View File

@@ -32,14 +32,17 @@ fun EpochScreen(
onEpochClick: (WeightEpoch) -> Unit,
onCreateNew: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(horizontal = 24.dp)
) {
Spacer(modifier = Modifier.height(60.dp))
Spacer(modifier = Modifier.height(56.dp))
// 标题
Text(
@@ -49,69 +52,86 @@ fun EpochScreen(
color = Slate900
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "追踪你的健康演变",
fontSize = 14.sp,
color = Slate500,
modifier = Modifier.padding(top = 4.dp)
fontSize = 15.sp,
color = Slate400
)
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(28.dp))
if (isLoading && epochs.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = Brand500)
when {
isLoading && epochs.isEmpty() -> {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = Brand500)
}
}
} else if (epochs.isEmpty()) {
// 空状态
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "还没有纪元",
fontSize = 16.sp,
color = Slate500
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onCreateNew,
colors = ButtonDefaults.buttonColors(containerColor = Slate900)
) {
Text("开启新纪元")
epochs.isEmpty() -> {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "还没有纪元",
fontSize = 16.sp,
color = Slate500
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onCreateNew,
colors = ButtonDefaults.buttonColors(containerColor = Slate900),
shape = RoundedCornerShape(12.dp)
) {
Text("开启新纪元", modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp))
}
}
}
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(epochs) { epoch ->
EpochCard(
epoch = epoch,
onClick = { onEpochClick(epoch) }
)
else -> {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(bottom = 100.dp)
) {
items(epochs, key = { it.id }) { epoch ->
EpochCard(
epoch = epoch,
onClick = { onEpochClick(epoch) }
)
}
}
}
}
}
// FAB
// FAB - 右下角圆形按钮
FloatingActionButton(
onClick = onCreateNew,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(24.dp),
.padding(end = 24.dp, bottom = 24.dp)
.size(56.dp),
containerColor = Slate900,
contentColor = Color.White,
shape = CircleShape
shape = CircleShape,
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 4.dp
)
) {
Icon(Icons.Default.Add, contentDescription = "创建纪元")
Icon(
Icons.Default.Add,
contentDescription = "创建纪元",
modifier = Modifier.size(24.dp)
)
}
}
}
@@ -133,9 +153,7 @@ private fun EpochCard(
val totalDays = ChronoUnit.DAYS.between(start, end).toFloat()
val passedDays = ChronoUnit.DAYS.between(start, now).toFloat()
(passedDays / totalDays).coerceIn(0f, 1f)
} catch (e: Exception) {
0f
}
} catch (e: Exception) { 0f }
}
// 格式化日期
@@ -150,30 +168,27 @@ private fun EpochCard(
}
}
Card(
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
.clip(RoundedCornerShape(16.dp))
.clickable { onClick() },
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Slate50),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
color = Slate50
) {
Row(modifier = Modifier.fillMaxWidth()) {
// 左侧绿色指示条
Box(
modifier = Modifier
.width(4.dp)
.height(100.dp)
.background(
if (isActive) Brand500 else Slate300,
RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp)
)
.height(108.dp)
.background(if (isActive) Brand500 else Slate200)
)
Column(
modifier = Modifier
.weight(1f)
.padding(16.dp)
.padding(start = 16.dp, end = 20.dp, top = 18.dp, bottom = 18.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -187,16 +202,14 @@ private fun EpochCard(
color = Slate900
)
// 状态标签
if (isCompleted) {
Icon(
when {
isCompleted -> Icon(
Icons.Default.Check,
contentDescription = "已完成",
tint = Brand500,
modifier = Modifier.size(24.dp)
tint = Brand500.copy(alpha = 0.6f),
modifier = Modifier.size(40.dp)
)
} else if (isActive) {
Text(
isActive -> Text(
text = "进行中",
fontSize = 14.sp,
color = Brand500,
@@ -205,15 +218,15 @@ private fun EpochCard(
}
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(6.dp))
Text(
text = dateRange,
fontSize = 14.sp,
color = Slate500
color = Slate400
)
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(16.dp))
// 进度条
Box(
@@ -227,9 +240,7 @@ private fun EpochCard(
modifier = Modifier
.fillMaxWidth(progress)
.fillMaxHeight()
.background(
if (isCompleted) Slate400 else Slate900
)
.background(Slate900)
)
}
}

View File

@@ -33,7 +33,6 @@ fun PlanScreen(
onCreateEpoch: () -> Unit
) {
if (activeEpoch == null) {
// 没有活跃纪元
EmptyPlanState(onCreateEpoch = onCreateEpoch)
return
}
@@ -42,24 +41,25 @@ fun PlanScreen(
val currentYear = now.year
val currentWeek = now.get(WeekFields.ISO.weekOfWeekBasedYear())
// 分离当前周和后续周
// 后续周计划
val futurePlans = weeklyPlans.filter { plan ->
plan.plan.year > currentYear ||
(plan.plan.year == currentYear && plan.plan.week > currentWeek)
}.take(5) // 只显示接下来5周
}.take(5)
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(horizontal = 24.dp)
.statusBarsPadding()
.padding(horizontal = 20.dp)
) {
Spacer(modifier = Modifier.height(60.dp))
Spacer(modifier = Modifier.height(12.dp))
// 标题
Text(
text = "计划中心",
fontSize = 28.sp,
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = Slate900
)
@@ -68,40 +68,38 @@ fun PlanScreen(
text = "专注于本阶段的周计划",
fontSize = 14.sp,
color = Slate500,
modifier = Modifier.padding(top = 4.dp)
modifier = Modifier.padding(top = 2.dp)
)
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(24.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(bottom = 24.dp)
) {
// 正在进行中
item {
Text(
text = "正在进行中",
fontSize = 14.sp,
color = Slate500,
modifier = Modifier.padding(bottom = 8.dp)
fontSize = 13.sp,
color = Slate500
)
Spacer(modifier = Modifier.height(8.dp))
}
// 当前周计划卡片
item {
currentWeekPlan?.let { plan ->
if (currentWeekPlan != null) {
CurrentWeekCard(
epoch = activeEpoch,
plan = plan,
epochDetail = epochDetail,
onClick = { onPlanClick(plan) }
plan = currentWeekPlan,
onClick = { onPlanClick(currentWeekPlan) }
)
} ?: run {
// 没有当前周计划
Card(
} else {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Slate50)
shape = RoundedCornerShape(12.dp),
color = Slate50
) {
Box(
modifier = Modifier
@@ -109,10 +107,7 @@ fun PlanScreen(
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "本周暂无计划",
color = Slate500
)
Text(text = "本周暂无计划", color = Slate500, fontSize = 14.sp)
}
}
}
@@ -121,20 +116,17 @@ fun PlanScreen(
// 后续序列
if (futurePlans.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "后续序列",
fontSize = 14.sp,
color = Slate500,
modifier = Modifier.padding(bottom = 8.dp)
fontSize = 13.sp,
color = Slate500
)
Spacer(modifier = Modifier.height(4.dp))
}
items(futurePlans) { plan ->
FuturePlanItem(
plan = plan,
onClick = { onPlanClick(plan) }
)
FuturePlanItem(plan = plan, onClick = { onPlanClick(plan) })
}
}
}
@@ -146,7 +138,8 @@ private fun EmptyPlanState(onCreateEpoch: () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
.background(Color.White)
.statusBarsPadding(),
contentAlignment = Alignment.Center
) {
Column(
@@ -155,11 +148,11 @@ private fun EmptyPlanState(onCreateEpoch: () -> Unit) {
) {
Text(
text = "还没有进行中的纪元",
fontSize = 18.sp,
fontSize = 17.sp,
fontWeight = FontWeight.Medium,
color = Slate700
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "创建一个纪元来开始你的健康计划",
fontSize = 14.sp,
@@ -170,11 +163,12 @@ private fun EmptyPlanState(onCreateEpoch: () -> Unit) {
onClick = onCreateEpoch,
colors = ButtonDefaults.buttonColors(containerColor = Slate900),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth(0.6f)
modifier = Modifier.height(48.dp)
) {
Text(
text = "开启新纪元",
modifier = Modifier.padding(vertical = 4.dp)
fontSize = 15.sp,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
@@ -185,7 +179,6 @@ private fun EmptyPlanState(onCreateEpoch: () -> Unit) {
private fun CurrentWeekCard(
epoch: WeightEpoch,
plan: WeeklyPlanDetail,
epochDetail: EpochDetail?,
onClick: () -> Unit
) {
// 计算周内天数
@@ -218,32 +211,31 @@ private fun CurrentWeekCard(
try {
val epochStart = LocalDate.parse(epoch.startDate.take(10))
val planStart = LocalDate.parse(plan.plan.startDate.take(10))
val weeks = ChronoUnit.WEEKS.between(epochStart, planStart) + 1
weeks.toInt()
(ChronoUnit.WEEKS.between(epochStart, planStart) + 1).toInt()
} catch (e: Exception) { plan.plan.week }
}
Card(
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Slate50),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
.clip(RoundedCornerShape(12.dp))
.clickable { onClick() },
shape = RoundedCornerShape(12.dp),
color = Slate50
) {
Row(modifier = Modifier.fillMaxWidth()) {
// 左侧绿色指示条
Box(
modifier = Modifier
.width(4.dp)
.height(120.dp)
.background(Brand500, RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp))
.height(110.dp)
.background(Brand500)
)
Column(
modifier = Modifier
.weight(1f)
.padding(16.dp)
.padding(horizontal = 16.dp, vertical = 14.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -252,20 +244,20 @@ private fun CurrentWeekCard(
) {
Text(
text = "${epoch.name} - 第 $weekIndex",
fontSize = 18.sp,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = Slate900
)
Text(
text = "$dayInWeek",
fontSize = 14.sp,
fontSize = 13.sp,
color = Brand500,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(6.dp))
// 目标信息
val targetText = buildString {
@@ -278,18 +270,18 @@ private fun CurrentWeekCard(
}
Text(
text = targetText,
fontSize = 14.sp,
fontSize = 13.sp,
color = Slate600
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(14.dp))
// 进度条
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.clip(RoundedCornerShape(3.dp))
.height(5.dp)
.clip(RoundedCornerShape(2.5.dp))
.background(Slate200)
) {
Box(
@@ -309,48 +301,46 @@ private fun FuturePlanItem(
plan: WeeklyPlanDetail,
onClick: () -> Unit
) {
// 格式化日期
val dateRange = remember(plan) {
try {
val start = LocalDate.parse(plan.plan.startDate.take(10))
val end = LocalDate.parse(plan.plan.endDate.take(10))
val formatter = DateTimeFormatter.ofPattern("MM.dd")
"${start.format(formatter)} - ${end.format(formatter)}"
} catch (e: Exception) {
""
}
} catch (e: Exception) { "" }
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "${plan.plan.week}",
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = Slate800
)
Text(
text = dateRange,
fontSize = 13.sp,
color = Slate500
)
}
Text(
text = "${plan.plan.week}",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Slate800
)
Text(
text = dateRange,
fontSize = 14.sp,
color = Slate500
text = "待启动",
fontSize = 13.sp,
color = Slate400
)
}
Text(
text = "待启动",
fontSize = 14.sp,
color = Slate400
)
HorizontalDivider(color = Slate100, thickness = 1.dp)
}
HorizontalDivider(color = Slate100)
}
private fun formatWeight(weight: Double): String {

View File

@@ -31,28 +31,27 @@ fun ProfileScreen(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(horizontal = 24.dp)
.statusBarsPadding()
.padding(horizontal = 20.dp)
) {
Spacer(modifier = Modifier.height(60.dp))
Spacer(modifier = Modifier.height(12.dp))
// 标题
Text(
text = "个人中心",
fontSize = 28.sp,
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = Slate900
)
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(28.dp))
// 用户信息
Row(
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// 头像
Box(
modifier = Modifier
.size(72.dp)
.size(64.dp)
.clip(CircleShape)
.background(Slate100)
) {
@@ -64,14 +63,13 @@ fun ProfileScreen(
contentScale = ContentScale.Crop
)
} else {
// 默认头像占位
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = user?.nickname?.firstOrNull()?.toString() ?: "U",
fontSize = 28.sp,
fontSize = 24.sp,
fontWeight = FontWeight.Medium,
color = Slate400
)
@@ -79,29 +77,29 @@ fun ProfileScreen(
}
}
Spacer(modifier = Modifier.width(16.dp))
Spacer(modifier = Modifier.width(14.dp))
Column {
Text(
text = user?.nickname ?: "用户",
fontSize = 22.sp,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = Slate900
)
Text(
text = "坚持 ${stats.persistDays}",
fontSize = 14.sp,
fontSize = 13.sp,
color = Slate500
)
}
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(20.dp))
// 统计卡片
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
StatCard(
title = "今年减重",
@@ -119,7 +117,7 @@ fun ProfileScreen(
)
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
// 年度进度卡片
YearProgressCard(daysRemaining = stats.daysRemaining)
@@ -134,13 +132,13 @@ fun ProfileScreen(
Text(
text = "退出登录",
color = ErrorRed,
fontSize = 16.sp
fontSize = 15.sp
)
}
HorizontalDivider(color = Slate100)
HorizontalDivider(color = Slate100, thickness = 1.dp)
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(24.dp))
}
}
@@ -152,36 +150,31 @@ private fun StatCard(
valueColor: Color,
modifier: Modifier = Modifier
) {
Card(
Surface(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Slate50),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
shape = RoundedCornerShape(12.dp),
color = Slate50
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Column(modifier = Modifier.padding(14.dp)) {
Text(
text = title,
fontSize = 14.sp,
fontSize = 13.sp,
color = Slate500
)
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.Bottom
) {
Spacer(modifier = Modifier.height(6.dp))
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = value,
fontSize = 28.sp,
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = valueColor
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(3.dp))
Text(
text = unit,
fontSize = 16.sp,
fontSize = 14.sp,
color = valueColor,
modifier = Modifier.padding(bottom = 4.dp)
modifier = Modifier.padding(bottom = 3.dp)
)
}
}
@@ -192,56 +185,51 @@ private fun StatCard(
private fun YearProgressCard(daysRemaining: Int) {
val currentYear = LocalDate.now().year
Card(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Navy900),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
shape = RoundedCornerShape(12.dp),
color = Navy900
) {
Box(modifier = Modifier.fillMaxWidth()) {
// 右上角年份标签
Surface(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(12.dp),
shape = RoundedCornerShape(8.dp),
.padding(10.dp),
shape = RoundedCornerShape(6.dp),
color = Slate700
) {
Text(
text = "$currentYear PROGRESS",
fontSize = 12.sp,
fontSize = 11.sp,
color = Slate300,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp)
)
}
Column(
modifier = Modifier.padding(24.dp)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(
text = "今年剩余天数",
fontSize = 14.sp,
fontSize = 13.sp,
color = Slate400
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(6.dp))
Row(
verticalAlignment = Alignment.Bottom
) {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = String.format("%02d", daysRemaining),
fontSize = 56.sp,
fontSize = 52.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "DAYS",
fontSize = 20.sp,
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = Slate400,
modifier = Modifier.padding(bottom = 12.dp)
modifier = Modifier.padding(bottom = 10.dp)
)
}
}

View File

@@ -14,7 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.healthflow.app.data.model.WeeklyPlanDetail
@@ -29,7 +28,9 @@ fun WeekPlanDetailScreen(
) {
if (planDetail == null) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.background(Color.White),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = Brand500)
@@ -41,42 +42,44 @@ fun WeekPlanDetailScreen(
var showWeightDialog by remember { mutableStateOf(false) }
var showTargetDialog by remember { mutableStateOf(false) }
// 计算周序号(简化处理)
val weekIndex = plan.week
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
.padding(horizontal = 24.dp)
.statusBarsPadding()
.padding(horizontal = 20.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
// 返回按钮
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable(onClick = onBack)
modifier = Modifier
.clickable(onClick = onBack)
.padding(vertical = 8.dp)
) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "返回",
tint = Slate600,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "返回",
fontSize = 16.sp,
fontSize = 15.sp,
color = Slate600
)
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(20.dp))
// 标题
Text(
text = "${weekIndex}周计划",
fontSize = 28.sp,
fontSize = 26.sp,
fontWeight = FontWeight.Bold,
color = Slate900
)
@@ -85,7 +88,7 @@ fun WeekPlanDetailScreen(
text = "正在为了本周目标努力",
fontSize = 14.sp,
color = Slate500,
modifier = Modifier.padding(top = 4.dp)
modifier = Modifier.padding(top = 2.dp)
)
Spacer(modifier = Modifier.height(48.dp))
@@ -95,21 +98,19 @@ fun WeekPlanDetailScreen(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.Bottom
) {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = plan.finalWeight?.let { formatWeight(it) } ?: "--",
fontSize = 72.sp,
fontSize = 64.sp,
fontWeight = FontWeight.Bold,
color = Slate900
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "kg",
fontSize = 24.sp,
fontSize = 22.sp,
color = Slate500,
modifier = Modifier.padding(bottom = 12.dp)
modifier = Modifier.padding(bottom = 10.dp)
)
}
@@ -120,38 +121,37 @@ fun WeekPlanDetailScreen(
)
}
Spacer(modifier = Modifier.height(48.dp))
Spacer(modifier = Modifier.height(40.dp))
// 操作按钮
Button(
onClick = { showWeightDialog = true },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
.height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(containerColor = Slate900)
) {
Text(
text = "添加体重记录",
fontSize = 16.sp,
fontSize = 15.sp,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(10.dp))
OutlinedButton(
onClick = { showTargetDialog = true },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Slate900),
border = ButtonDefaults.outlinedButtonBorder.copy(width = 1.dp)
.height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Slate900)
) {
Text(
text = "修改周目标",
fontSize = 16.sp,
fontSize = 15.sp,
fontWeight = FontWeight.Medium
)
}
@@ -161,50 +161,50 @@ fun WeekPlanDetailScreen(
// 本周指标
Text(
text = "本周指标",
fontSize = 14.sp,
fontSize = 13.sp,
color = Slate500
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
// 周初始
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
.padding(vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "周初始",
fontSize = 16.sp,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = Slate800
)
Text(
text = plan.initialWeight?.let { "${formatWeight(it)} kg" } ?: "--",
fontSize = 16.sp,
fontSize = 15.sp,
color = Slate600
)
}
HorizontalDivider(color = Slate100)
HorizontalDivider(color = Slate100, thickness = 1.dp)
// 周目标
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
.padding(vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "周目标",
fontSize = 16.sp,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = Slate800
)
Text(
text = plan.targetWeight?.let { "${formatWeight(it)} kg" } ?: "--",
fontSize = 16.sp,
fontSize = 15.sp,
color = Slate600
)
}
@@ -249,10 +249,7 @@ private fun WeightInputDialog(
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = title,
fontWeight = FontWeight.SemiBold
)
Text(text = title, fontWeight = FontWeight.SemiBold, fontSize = 17.sp)
},
text = {
OutlinedTextField(
@@ -261,24 +258,24 @@ private fun WeightInputDialog(
label = { Text("体重 (kg)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(10.dp)
)
},
confirmButton = {
TextButton(
onClick = {
weightText.toDoubleOrNull()?.let { onConfirm(it) }
},
onClick = { weightText.toDoubleOrNull()?.let { onConfirm(it) } },
enabled = weightText.toDoubleOrNull() != null
) {
Text("确定", color = Brand500)
Text("确定", color = Brand500, fontSize = 15.sp)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("取消", color = Slate500)
Text("取消", color = Slate500, fontSize = 15.sp)
}
}
},
shape = RoundedCornerShape(16.dp)
)
}
@@ -295,10 +292,7 @@ private fun TargetEditDialog(
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = "修改周目标",
fontWeight = FontWeight.SemiBold
)
Text(text = "修改周目标", fontWeight = FontWeight.SemiBold, fontSize = 17.sp)
},
text = {
Column {
@@ -308,36 +302,36 @@ private fun TargetEditDialog(
label = { Text("周初始体重 (kg)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(10.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(14.dp))
OutlinedTextField(
value = targetText,
onValueChange = { targetText = it },
label = { Text("周目标体重 (kg)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(10.dp)
)
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm(
initialText.toDoubleOrNull(),
targetText.toDoubleOrNull()
)
onConfirm(initialText.toDoubleOrNull(), targetText.toDoubleOrNull())
}
) {
Text("确定", color = Brand500)
Text("确定", color = Brand500, fontSize = 15.sp)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("取消", color = Slate500)
Text("取消", color = Slate500, fontSize = 15.sp)
}
}
},
shape = RoundedCornerShape(16.dp)
)
}