增加编辑和删除纪元的功能
This commit is contained in:
@@ -13,11 +13,11 @@ android {
|
||||
applicationId = "com.healthflow.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 44
|
||||
versionName = "2.2.1"
|
||||
versionCode = 45
|
||||
versionName = "2.2.2"
|
||||
|
||||
buildConfigField("String", "API_BASE_URL", "\"https://health.amos.us.kg/api/\"")
|
||||
buildConfigField("int", "VERSION_CODE", "44")
|
||||
buildConfigField("int", "VERSION_CODE", "45")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -36,6 +36,15 @@ interface ApiService {
|
||||
@GET("epoch/{id}")
|
||||
suspend fun getEpochDetail(@Path("id") id: Long): Response<EpochDetail>
|
||||
|
||||
@PUT("epoch/{id}")
|
||||
suspend fun updateEpoch(
|
||||
@Path("id") id: Long,
|
||||
@Body request: UpdateEpochRequest
|
||||
): Response<MessageResponse>
|
||||
|
||||
@DELETE("epoch/{id}")
|
||||
suspend fun deleteEpoch(@Path("id") id: Long): Response<MessageResponse>
|
||||
|
||||
@GET("epoch/{id}/plans")
|
||||
suspend fun getWeeklyPlans(@Path("id") epochId: Long): Response<List<WeeklyPlanDetail>>
|
||||
|
||||
|
||||
@@ -95,6 +95,15 @@ data class CreateEpochRequest(
|
||||
@SerialName("end_date") val endDate: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateEpochRequest(
|
||||
val name: String? = null,
|
||||
@SerialName("initial_weight") val initialWeight: Double? = null,
|
||||
@SerialName("target_weight") val targetWeight: Double? = null,
|
||||
@SerialName("start_date") val startDate: String? = null,
|
||||
@SerialName("end_date") val endDate: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EpochDetail(
|
||||
val epoch: WeightEpoch,
|
||||
|
||||
@@ -149,6 +149,16 @@ fun MainNavigation(
|
||||
activeEpoch?.let { epoch ->
|
||||
navController.navigate(Routes.weekPlanDetail(epoch.id, planDetail.plan.id))
|
||||
}
|
||||
},
|
||||
onEditEpoch = { epochId, name, initial, target, start, end ->
|
||||
epochViewModel.updateEpoch(epochId, name, initial, target, start, end) {
|
||||
// 更新成功后刷新数据
|
||||
}
|
||||
},
|
||||
onDeleteEpoch = { epochId ->
|
||||
epochViewModel.deleteEpoch(epochId) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package com.healthflow.app.ui.screen
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
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.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
|
||||
@@ -17,7 +21,9 @@ 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
|
||||
@@ -26,7 +32,7 @@ import com.healthflow.app.data.model.WeightEpoch
|
||||
import com.healthflow.app.ui.theme.*
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun EpochDetailScreen(
|
||||
@@ -34,8 +40,13 @@ fun EpochDetailScreen(
|
||||
epochDetail: EpochDetail?,
|
||||
weeklyPlans: List<WeeklyPlanDetail>,
|
||||
onBack: () -> Unit,
|
||||
onWeekClick: (WeeklyPlanDetail) -> Unit
|
||||
onWeekClick: (WeeklyPlanDetail) -> Unit,
|
||||
onEditEpoch: (Long, String?, Double?, Double?, String?, String?) -> Unit = { _, _, _, _, _, _ -> },
|
||||
onDeleteEpoch: (Long) -> Unit = {}
|
||||
) {
|
||||
var showEditDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (epoch == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -48,7 +59,6 @@ fun EpochDetailScreen(
|
||||
return
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
val dateRange = remember(epoch) {
|
||||
try {
|
||||
val start = LocalDate.parse(epoch.startDate.take(10))
|
||||
@@ -60,8 +70,6 @@ fun EpochDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 计算已减重
|
||||
val actualLoss = epochDetail?.actualChange ?: 0.0
|
||||
|
||||
Column(
|
||||
@@ -73,20 +81,44 @@ fun EpochDetailScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 返回按钮
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
contentDescription = "返回",
|
||||
tint = Slate900,
|
||||
modifier = Modifier
|
||||
.offset(x = (-8).dp)
|
||||
.size(32.dp)
|
||||
.clickable(onClick = onBack)
|
||||
)
|
||||
// 顶部栏:返回 + 编辑/删除
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
contentDescription = "返回",
|
||||
tint = Slate900,
|
||||
modifier = Modifier
|
||||
.offset(x = (-8).dp)
|
||||
.size(32.dp)
|
||||
.clickable(onClick = onBack)
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
Icons.Outlined.Edit,
|
||||
contentDescription = "编辑",
|
||||
tint = Slate500,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable { showEditDialog = true }
|
||||
)
|
||||
Icon(
|
||||
Icons.Outlined.Delete,
|
||||
contentDescription = "删除",
|
||||
tint = ErrorRed,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable { showDeleteDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 纪元名称
|
||||
Text(
|
||||
text = epoch.name.ifEmpty { "未命名纪元" },
|
||||
fontSize = 28.sp,
|
||||
@@ -97,7 +129,6 @@ fun EpochDetailScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// 日期范围
|
||||
Text(
|
||||
text = dateRange,
|
||||
fontSize = 14.sp,
|
||||
@@ -106,7 +137,6 @@ fun EpochDetailScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// 统计数据
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
@@ -128,7 +158,6 @@ fun EpochDetailScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
// 每周列表
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
contentPadding = PaddingValues(bottom = 24.dp)
|
||||
@@ -137,12 +166,200 @@ fun EpochDetailScreen(
|
||||
WeekItem(
|
||||
weekIndex = index + 1,
|
||||
planDetail = planDetail,
|
||||
epoch = epoch,
|
||||
onClick = { onWeekClick(planDetail) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑弹窗
|
||||
if (showEditDialog) {
|
||||
EditEpochDialog(
|
||||
epoch = epoch,
|
||||
onDismiss = { showEditDialog = false },
|
||||
onConfirm = { name, initial, target, start, end ->
|
||||
onEditEpoch(epoch.id, name, initial, target, start, end)
|
||||
showEditDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 删除确认弹窗
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
containerColor = Color.White,
|
||||
title = {
|
||||
Text("删除纪元", fontWeight = FontWeight.SemiBold)
|
||||
},
|
||||
text = {
|
||||
Text("确定要删除「${epoch.name.ifEmpty { "未命名纪元" }}」吗?此操作不可恢复。")
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDeleteEpoch(epoch.id)
|
||||
showDeleteDialog = false
|
||||
}) {
|
||||
Text("删除", color = ErrorRed, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteDialog = false }) {
|
||||
Text("取消", color = Slate500)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditEpochDialog(
|
||||
epoch: WeightEpoch,
|
||||
onDismiss: () -> Unit,
|
||||
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,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
containerColor = Color.White,
|
||||
title = {
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
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) }
|
||||
)
|
||||
}) {
|
||||
Text("保存", color = Brand500, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("取消", color = Slate500)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -167,24 +384,16 @@ private fun StatItem(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 周状态枚举
|
||||
private enum class WeekStatus {
|
||||
PAST, // 已过去
|
||||
CURRENT, // 进行中
|
||||
FUTURE // 未开启
|
||||
}
|
||||
private enum class WeekStatus { PAST, CURRENT, FUTURE }
|
||||
|
||||
@Composable
|
||||
private fun WeekItem(
|
||||
weekIndex: Int,
|
||||
planDetail: WeeklyPlanDetail,
|
||||
epoch: WeightEpoch,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val plan = planDetail.plan
|
||||
|
||||
// 格式化日期
|
||||
val dateRange = remember(plan) {
|
||||
try {
|
||||
val start = LocalDate.parse(plan.startDate.take(10))
|
||||
@@ -194,41 +403,32 @@ private fun WeekItem(
|
||||
} catch (e: Exception) { "" }
|
||||
}
|
||||
|
||||
// 判断周状态
|
||||
val weekStatus = remember(plan) {
|
||||
try {
|
||||
val now = LocalDate.now()
|
||||
val start = LocalDate.parse(plan.startDate.take(10))
|
||||
val end = LocalDate.parse(plan.endDate.take(10))
|
||||
when {
|
||||
now.isAfter(end) -> WeekStatus.PAST // 当前日期在结束日期之后 = 已过去
|
||||
now.isBefore(start) -> WeekStatus.FUTURE // 当前日期在开始日期之前 = 未开启
|
||||
else -> WeekStatus.CURRENT // 正在进行中
|
||||
now.isAfter(end) -> WeekStatus.PAST
|
||||
now.isBefore(start) -> WeekStatus.FUTURE
|
||||
else -> WeekStatus.CURRENT
|
||||
}
|
||||
} catch (e: Exception) { WeekStatus.CURRENT }
|
||||
}
|
||||
|
||||
// 计算本周变化
|
||||
val weightChange = planDetail.weightChange
|
||||
val hasRecord = plan.finalWeight != null
|
||||
val hasTarget = plan.targetWeight != null
|
||||
|
||||
// 只有已过去的周才显示合格/不合格印章
|
||||
val showStamp = weekStatus == WeekStatus.PAST && hasTarget
|
||||
val isQualified = hasRecord && hasTarget && planDetail.isQualified
|
||||
val isFailed = hasTarget && (!hasRecord || !planDetail.isQualified)
|
||||
|
||||
// 未开启的周不允许点击
|
||||
val isClickable = weekStatus != WeekStatus.FUTURE
|
||||
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (isClickable) Modifier.clickable(onClick = onClick)
|
||||
else Modifier
|
||||
)
|
||||
.then(if (isClickable) Modifier.clickable(onClick = onClick) else Modifier)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -251,10 +451,8 @@ private fun WeekItem(
|
||||
)
|
||||
}
|
||||
|
||||
// 右侧显示状态
|
||||
when (weekStatus) {
|
||||
WeekStatus.PAST -> {
|
||||
// 已过去的周:显示体重变化
|
||||
if (hasRecord && weightChange != null) {
|
||||
Text(
|
||||
text = if (weightChange >= 0) "-${formatWeight(weightChange)} kg" else "+${formatWeight(-weightChange)} kg",
|
||||
@@ -263,89 +461,38 @@ private fun WeekItem(
|
||||
color = if (weightChange >= 0) Brand500 else ErrorRed
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "未记录",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Slate400
|
||||
)
|
||||
Text(text = "未记录", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Slate400)
|
||||
}
|
||||
}
|
||||
WeekStatus.CURRENT -> {
|
||||
// 进行中
|
||||
Text(
|
||||
text = "进行中",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Brand500
|
||||
)
|
||||
Text(text = "进行中", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Brand500)
|
||||
}
|
||||
WeekStatus.FUTURE -> {
|
||||
// 未开启
|
||||
Text(
|
||||
text = "未开启",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Slate300
|
||||
)
|
||||
Text(text = "未开启", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Slate300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 印章 - 只有已过去且有目标的周才显示
|
||||
if (showStamp) {
|
||||
val stampModifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 80.dp)
|
||||
.rotate(-15f)
|
||||
.alpha(0.12f)
|
||||
|
||||
if (isQualified) {
|
||||
// 合格印章
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 80.dp)
|
||||
.rotate(-15f)
|
||||
.alpha(0.12f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Brand500,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Text(
|
||||
text = "合格",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
letterSpacing = 1.sp,
|
||||
color = Brand500
|
||||
)
|
||||
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 = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 80.dp)
|
||||
.rotate(-15f)
|
||||
.alpha(0.12f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
tint = ErrorRed,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Text(
|
||||
text = "不合格",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
letterSpacing = 1.sp,
|
||||
color = ErrorRed
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(color = Slate200, thickness = 1.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +237,69 @@ class EpochViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEpoch(
|
||||
epochId: Long,
|
||||
name: String?,
|
||||
initialWeight: Double?,
|
||||
targetWeight: Double?,
|
||||
startDate: String?,
|
||||
endDate: String?,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
_error.value = null
|
||||
try {
|
||||
val request = UpdateEpochRequest(
|
||||
name = name,
|
||||
initialWeight = initialWeight,
|
||||
targetWeight = targetWeight,
|
||||
startDate = startDate,
|
||||
endDate = endDate
|
||||
)
|
||||
val response = api.updateEpoch(epochId, request)
|
||||
if (response.isSuccessful) {
|
||||
loadAll()
|
||||
onSuccess()
|
||||
} else {
|
||||
_error.value = "更新失败"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_error.value = e.message
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEpoch(epochId: Long, onSuccess: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
_error.value = null
|
||||
try {
|
||||
val response = api.deleteEpoch(epochId)
|
||||
if (response.isSuccessful) {
|
||||
// 从列表中移除
|
||||
_epochList.value = _epochList.value.filter { it.id != epochId }
|
||||
// 如果删除的是活跃纪元,清空相关状态
|
||||
if (_activeEpoch.value?.id == epochId) {
|
||||
_activeEpoch.value = null
|
||||
_epochDetail.value = null
|
||||
_weeklyPlans.value = emptyList()
|
||||
_currentWeekPlan.value = null
|
||||
}
|
||||
onSuccess()
|
||||
} else {
|
||||
_error.value = "删除失败"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_error.value = e.message
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
@@ -150,6 +150,74 @@ func (h *EpochHandler) GetEpochList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, epochs)
|
||||
}
|
||||
|
||||
// 更新纪元
|
||||
func (h *EpochHandler) UpdateEpoch(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
epochID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
|
||||
var req model.UpdateEpochRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查纪元是否存在且属于当前用户
|
||||
var exists int
|
||||
err := h.db.QueryRow("SELECT 1 FROM weight_epochs WHERE id = ? AND user_id = ?", epochID, userID).Scan(&exists)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "epoch not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新纪元
|
||||
_, err = h.db.Exec(`
|
||||
UPDATE weight_epochs
|
||||
SET name = COALESCE(?, name),
|
||||
initial_weight = COALESCE(?, initial_weight),
|
||||
target_weight = COALESCE(?, target_weight),
|
||||
start_date = COALESCE(?, start_date),
|
||||
end_date = COALESCE(?, end_date)
|
||||
WHERE id = ? AND user_id = ?
|
||||
`, req.Name, req.InitialWeight, req.TargetWeight, req.StartDate, req.EndDate, epochID, userID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update epoch"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "epoch updated"})
|
||||
}
|
||||
|
||||
// 删除纪元
|
||||
func (h *EpochHandler) DeleteEpoch(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
epochID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
|
||||
// 检查纪元是否存在且属于当前用户
|
||||
var exists int
|
||||
err := h.db.QueryRow("SELECT 1 FROM weight_epochs WHERE id = ? AND user_id = ?", epochID, userID).Scan(&exists)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "epoch not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 先删除关联的周计划
|
||||
_, err = h.db.Exec("DELETE FROM weekly_plans WHERE epoch_id = ? AND user_id = ?", epochID, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete weekly plans"})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除纪元
|
||||
_, err = h.db.Exec("DELETE FROM weight_epochs WHERE id = ? AND user_id = ?", epochID, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete epoch"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "epoch deleted"})
|
||||
}
|
||||
|
||||
// 获取纪元的每周计划列表
|
||||
func (h *EpochHandler) GetWeeklyPlans(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
@@ -155,6 +155,14 @@ type UpdateWeeklyPlanRequest struct {
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
type UpdateEpochRequest struct {
|
||||
Name *string `json:"name"`
|
||||
InitialWeight *float64 `json:"initial_weight"`
|
||||
TargetWeight *float64 `json:"target_weight"`
|
||||
StartDate *string `json:"start_date"`
|
||||
EndDate *string `json:"end_date"`
|
||||
}
|
||||
|
||||
// 体重相关请求 (保留兼容)
|
||||
type SetWeightGoalRequest struct {
|
||||
TargetWeight float64 `json:"target_weight" binding:"required,gt=0"`
|
||||
|
||||
@@ -71,6 +71,8 @@ func Setup(db *sql.DB, cfg *config.Config) *gin.Engine {
|
||||
auth.GET("/epoch/active", epochHandler.GetActiveEpoch)
|
||||
auth.GET("/epoch/list", epochHandler.GetEpochList)
|
||||
auth.GET("/epoch/:id", epochHandler.GetEpochDetail)
|
||||
auth.PUT("/epoch/:id", epochHandler.UpdateEpoch)
|
||||
auth.DELETE("/epoch/:id", epochHandler.DeleteEpoch)
|
||||
auth.GET("/epoch/:id/plans", epochHandler.GetWeeklyPlans)
|
||||
auth.PUT("/epoch/:id/plan/:planId", epochHandler.UpdateWeeklyPlan)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user