diff --git a/shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt b/shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt index 78901e6..1d2ce64 100644 --- a/shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt +++ b/shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt @@ -208,6 +208,10 @@ class DeviceManager() : IDeviceManager(), OnBleConnectionListener, OnSleepDataLo return app.bleManager.connectedDevice?.address } + override fun setAutoConnect(enable: Boolean) { + enableAutoConnect = enable + } + override fun startScan() { val bluetoothAdapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter diff --git a/shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt b/shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt index c013587..cf72dc8 100644 --- a/shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt +++ b/shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt @@ -1,6 +1,7 @@ package com.whitefish.ring.data import com.whitefish.ring.bean.ui.HeartRate +import com.whitefish.ring.bean.ui.SleepState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext @@ -26,4 +27,12 @@ actual suspend fun getHeartRate( } } } +} + +actual suspend fun getSleep( + start: Long, + end: Long +): SleepState { + //todo android sleep data + return SleepState(0,emptyList()) } \ No newline at end of file diff --git a/shared/src/commonMain/composeResources/drawable/bg_recovery_chart.png b/shared/src/commonMain/composeResources/drawable/bg_recovery_chart.png new file mode 100644 index 0000000..41abbf0 Binary files /dev/null and b/shared/src/commonMain/composeResources/drawable/bg_recovery_chart.png differ diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/App.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/App.kt index 8b2470f..8802d3d 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/App.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/App.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import com.whitefish.ring.device.IDeviceManager +import com.whitefish.ring.ui.chart.RecoveryChartDemo import com.whitefish.ring.ui.guide.GuideNavigationScreen import com.whitefish.ring.ui.home.HomeScreen import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt index b5e8a01..d030785 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt @@ -14,7 +14,7 @@ abstract class IDeviceManager { private val bleStateListeners = arrayListOf<(Int) -> Unit>() protected var onResetDeviceWhenBind:()-> Unit = {} - protected var autoConnect = false + protected var enableAutoConnect = false fun bleStateListeners() = bleStateListeners diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChart.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChart.kt new file mode 100644 index 0000000..0eba0fe --- /dev/null +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChart.kt @@ -0,0 +1,284 @@ +package com.whitefish.ring.ui.chart + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.Canvas +import androidx.compose.ui.geometry.Offset +import kotlin.math.* + +@Composable +fun RecoveryChart( + modifier: Modifier = Modifier, + currentHour: Float = 18f, + recoveryData: List = generateSampleRecoveryData() +) { + Card( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)), + shape = RoundedCornerShape(24.dp) + ) { + Box( + modifier = Modifier + .background( + Brush.radialGradient( + colors = listOf( + Color(0xFF8B7ED8), + Color(0xFF6B5B95) + ), + radius = 800f + ) + ) + .padding(24.dp) + ) { + Column { + // 标题和箭头 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "恢复曲线", + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Medium + ) + Text( + text = "→", + color = Color.White.copy(alpha = 0.8f), + fontSize = 24.sp + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 图表区域 + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + ) { + drawRecoveryChart( + size = size, + recoveryData = recoveryData, + currentHour = currentHour + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 建议文本 + Box( + modifier = Modifier + .fillMaxWidth() + .background( + Color.White.copy(alpha = 0.15f), + RoundedCornerShape(16.dp) + ) + .padding(16.dp) + ) { + Text( + text = "你当天的身体状态似乎不太好,请注意保持适量的运动,晚上早点休息,养成良好的锻炼和睡眠规律。", + color = Color.White.copy(alpha = 0.9f), + fontSize = 14.sp, + lineHeight = 20.sp + ) + } + } + } + } +} + +private fun DrawScope.drawRecoveryChart( + size: androidx.compose.ui.geometry.Size, + recoveryData: List, + currentHour: Float +) { + val padding = 40f + val chartWidth = size.width - padding * 2 + val chartHeight = size.height - padding * 2 + val chartBottom = size.height - padding + + // 绘制水平虚线 + val midLineY = chartBottom - chartHeight / 2 + drawDashedLine( + start = Offset(padding, midLineY), + end = Offset(size.width - padding, midLineY), + color = Color.White.copy(alpha = 0.4f) + ) + + // 绘制时间标签 + val timeLabels = listOf("00:00", "6:00", "12:00", "18:00", "24:00") + timeLabels.forEachIndexed { index, label -> + val x = padding + (chartWidth / (timeLabels.size - 1)) * index + drawContext.canvas.nativeCanvas.apply { + // 注意:这里需要平台特定的实现来绘制文本 + // 在实际实现中,你可能需要使用TextPaint或其他方式 + } + } + + // 准备路径数据 + val points = recoveryData.mapIndexed { index, value -> + val x = padding + (chartWidth / (recoveryData.size - 1)) * index + val y = chartBottom - (value * chartHeight) + Offset(x, y) + } + + // 找到当前时间对应的点索引 + val currentIndex = (currentHour * recoveryData.size / 24f).toInt() + .coerceIn(0, recoveryData.size - 1) + + // 绘制主要恢复曲线(渐变色) + if (points.isNotEmpty()) { + val gradientBrush = Brush.linearGradient( + colors = listOf( + Color(0xFF4CAF50), // 绿色 + Color(0xFFFFEB3B), // 黄色 + Color(0xFFFF9800) // 橙色 + ), + start = Offset(points.first().x, 0f), + end = Offset(points[currentIndex].x, 0f) + ) + + // 绘制实际曲线(到当前时间) + drawSmoothCurve( + points = points.take(currentIndex + 1), + brush = gradientBrush, + strokeWidth = 4.dp.toPx() + ) + + // 绘制预测曲线(白色虚线) + if (currentIndex < points.size - 1) { + drawSmoothCurve( + points = points.drop(currentIndex), + color = Color.White.copy(alpha = 0.8f), + strokeWidth = 3.dp.toPx(), + isDashed = true + ) + } + + // 绘制当前位置的白色亮点 + drawCircle( + color = Color.White, + radius = 8.dp.toPx(), + center = points[currentIndex] + ) + drawCircle( + color = Color.White.copy(alpha = 0.3f), + radius = 12.dp.toPx(), + center = points[currentIndex] + ) + } +} + +private fun DrawScope.drawDashedLine( + start: Offset, + end: Offset, + color: Color, + dashWidth: Float = 8f, + gapWidth: Float = 8f +) { + val totalDistance = (end - start).getDistance() + val dashCount = (totalDistance / (dashWidth + gapWidth)).toInt() + + repeat(dashCount) { index -> + val startRatio = index * (dashWidth + gapWidth) / totalDistance + val endRatio = (index * (dashWidth + gapWidth) + dashWidth) / totalDistance + + if (endRatio <= 1f) { + val dashStart = start + (end - start) * startRatio + val dashEnd = start + (end - start) * endRatio.coerceAtMost(1f) + + drawLine( + color = color, + start = dashStart, + end = dashEnd, + strokeWidth = 2.dp.toPx() + ) + } + } +} + +private fun DrawScope.drawSmoothCurve( + points: List, + brush: Brush? = null, + color: Color = Color.Transparent, + strokeWidth: Float = 4f, + isDashed: Boolean = false +) { + if (points.size < 2) return + + val path = Path() + path.moveTo(points.first().x, points.first().y) + + // 使用二次贝塞尔曲线创建平滑路径 + for (i in 1 until points.size) { + val currentPoint = points[i] + val previousPoint = points[i - 1] + + val midPointX = (previousPoint.x + currentPoint.x) / 2 + val midPointY = (previousPoint.y + currentPoint.y) / 2 + + if (i == 1) { + path.lineTo(midPointX, midPointY) + } else { + path.quadraticTo(previousPoint.x, previousPoint.y, midPointX, midPointY) + } + + if (i == points.size - 1) { + path.lineTo(currentPoint.x, currentPoint.y) + } + } + + val pathEffect = if (isDashed) { + PathEffect.dashPathEffect(floatArrayOf(12f, 8f)) + } else null + + if (brush != null) { + drawPath( + path = path, + brush = brush, + style = Stroke( + width = strokeWidth, + pathEffect = pathEffect + ) + ) + } else { + drawPath( + path = path, + color = color, + style = Stroke( + width = strokeWidth, + pathEffect = pathEffect + ) + ) + } +} + +// 生成示例恢复数据 +private fun generateSampleRecoveryData(): List { + // 24小时的恢复数据点,模拟一天的身体状态变化 + return (0..47).map { hour -> + val timeInHours = hour * 0.5f + when { + timeInHours < 6f -> 0.2f + timeInHours * 0.05f // 夜间恢复 + timeInHours < 12f -> 0.5f + sin((timeInHours - 6f) * PI / 6f).toFloat() * 0.3f // 上午活跃 + timeInHours < 18f -> 0.8f - (timeInHours - 12f) * 0.05f // 下午下降 + else -> 0.5f - sin((timeInHours - 18f) * PI / 6f).toFloat() * 0.2f // 晚间 + }.coerceIn(0f, 1f) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartDemo.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartDemo.kt new file mode 100644 index 0000000..5e9c7ba --- /dev/null +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartDemo.kt @@ -0,0 +1,190 @@ +package com.whitefish.ring.ui.chart + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +// import kotlinx.datetime.* + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RecoveryChartDemo( + modifier: Modifier = Modifier +) { + var currentHour by remember { mutableFloatStateOf(18f) } + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "恢复曲线图表演示", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + // 时间控制器 + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "当前时间: ${formatTime(currentHour)}", + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + + Slider( + value = currentHour, + onValueChange = { currentHour = it }, + valueRange = 0f..24f, + steps = 47 // 24小时 * 2 - 1 (每30分钟一步) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("00:00", fontSize = 12.sp, color = Color.Gray) + Text("06:00", fontSize = 12.sp, color = Color.Gray) + Text("12:00", fontSize = 12.sp, color = Color.Gray) + Text("18:00", fontSize = 12.sp, color = Color.Gray) + Text("24:00", fontSize = 12.sp, color = Color.Gray) + } + } + } + + // 第一个图表 - 基础版本 + Text( + text = "基础恢复曲线", + fontSize = 18.sp, + fontWeight = FontWeight.Medium + ) + + RecoveryChart( + modifier = Modifier.fillMaxWidth(), + currentHour = currentHour + ) + + // 第二个图表 - 带时间标签版本 + Text( + text = "带时间标签的恢复曲线", + fontSize = 18.sp, + fontWeight = FontWeight.Medium + ) + + RecoveryChartWithTimeLabels( + modifier = Modifier.fillMaxWidth(), + actualData = arrayListOf(1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f), + predictedData = arrayListOf() + ) + + // 功能说明 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFF5F5F5) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "图表特性说明", + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + + val features = listOf( + "🌈 渐变色线条:从绿色到橙色的动态渐变", + "⭐ 白色亮点:标记当前时间位置,带光晕效果", + "📊 预测线段:白色线条显示未来预测数据", + "⏰ 时间刻度:准确的24小时时间标记", + "🎨 紫色背景:符合设计稿的渐变背景", + "📱 响应式设计:适配不同屏幕尺寸" + ) + + features.forEach { feature -> + Text( + text = feature, + fontSize = 14.sp, + color = Color.DarkGray + ) + } + } + } + + // 数据说明 + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFE3F2FD) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "数据模型说明", + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + + Text( + text = "• 夜间 (00:00-06:00): 低水平,逐渐恢复\n" + + "• 上午 (06:00-12:00): 快速上升至峰值\n" + + "• 下午 (12:00-18:00): 从峰值逐渐下降\n" + + "• 晚上 (18:00-24:00): 持续下降至夜间水平", + fontSize = 14.sp, + color = Color.DarkGray + ) + } + } + } +} + +private fun formatTime(hour: Float): String { + val hours = hour.toInt() + val minutes = ((hour - hours) * 60).toInt() + return "${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}" +} + +// 根据当前时间生成演示的实际数据 +private fun generateDemoActualData(currentHour: Int): List { + val actualHours = currentHour.coerceIn(1, 23) // 至少1小时,最多23小时 + return (0 until actualHours).map { hour -> + when { + hour < 6 -> 0.3f + hour * 0.05f // 夜间缓慢上升 + hour < 12 -> 0.6f + (hour - 6) * 0.05f // 上午继续上升 + else -> 0.9f - (hour - 12) * 0.03f // 下午缓慢下降 + }.coerceIn(0f, 1f) + } +} + +// 根据当前时间生成演示的预测数据 +private fun generateDemoPredictedData(currentHour: Int): List { + val startHour = currentHour.coerceIn(1, 23) + return (startHour until 24).map { hour -> + when { + hour < 18 -> 0.8f - (hour - startHour) * 0.02f // 继续下降 + hour < 22 -> 0.6f - (hour - 18) * 0.05f // 傍晚下降 + else -> 0.4f - (hour - 22) * 0.05f // 夜间低水平 + }.coerceIn(0f, 1f) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartWithTimeLabels.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartWithTimeLabels.kt new file mode 100644 index 0000000..2659f23 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartWithTimeLabels.kt @@ -0,0 +1,432 @@ +package com.whitefish.ring.ui.chart + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import org.jetbrains.compose.resources.painterResource +import ring.shared.generated.resources.Res +import ring.shared.generated.resources.bg_recovery_chart +import ring.shared.generated.resources.bg_recovery_score +import kotlin.math.* + +@Composable +fun RecoveryChartWithTimeLabels( + modifier: Modifier = Modifier, + actualData: List = generateSampleActualData(), // 实际数据(彩色曲线) + predictedData: List = generateSamplePredictedData() // 预测数据(白色曲线) +) { + val textMeasurer = rememberTextMeasurer() + + Card( + modifier = modifier + .clip(RoundedCornerShape(24.dp)), + shape = RoundedCornerShape(24.dp) + ) { + + Box(modifier = Modifier.wrapContentSize()) { + Image( + painterResource(Res.drawable.bg_recovery_chart), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + Box( + modifier = Modifier.padding(horizontal = 24.dp) + ) { + Column { + // 标题和箭头 + Row( + modifier = Modifier.fillMaxWidth().padding(top = 15.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "恢复曲线", + color = Color.White, + fontSize = 20.sp, + fontWeight = FontWeight.Medium + ) + Text( + text = "→", + color = Color.White.copy(alpha = 0.8f), + fontSize = 24.sp + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 图表区域 + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(83.dp) + ) { + drawRecoveryChartWithLabels( + size = size, + actualData = actualData, + predictedData = predictedData, + textMeasurer = textMeasurer + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + } + + + } + + Box(modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 13.dp).align(Alignment.BottomCenter)) { + // 建议文本 + Row( + modifier = Modifier + .fillMaxWidth() + .height(80.dp) + .background( + Color.White.copy(alpha = 0.15f), + RoundedCornerShape(16.dp) + ) + ) { + Text( + text = "你当天的身体状态似乎不太好,请注意保持适量的运动,晚上早点休息,养成良好的锻炼和睡眠规律。", + color = Color.White.copy(alpha = 0.9f), + fontSize = 14.sp, + lineHeight = 20.sp, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 16.dp) + ) + } + + } + } + + + } +} + +private fun DrawScope.drawRecoveryChartWithLabels( + size: androidx.compose.ui.geometry.Size, + actualData: List, + predictedData: List, + textMeasurer: TextMeasurer +) { + val padding = 24f + val bottomPadding = 30f // 为时间标签留出更多空间 + val chartWidth = size.width - padding * 2 + val chartHeight = size.height - padding - bottomPadding + val chartBottom = size.height - bottomPadding + + // 绘制水平虚线 + val midLineY = chartBottom - chartHeight / 2 + drawDashedLine( + start = Offset(padding, midLineY), + end = Offset(size.width - padding, midLineY), + color = Color.White.copy(alpha = 0.4f) + ) + + // 合并两组数据(实际数据 + 预测数据) + val allData = actualData + predictedData + val actualDataSize = actualData.size + + // 确保总共24个数据点(每小时一个点) + val totalExpectedPoints = 24 + val combinedData = if (allData.size == totalExpectedPoints) { + allData + } else { + // 如果数据点不是24个,进行插值或截取 + generateInterpolatedData(allData, totalExpectedPoints) + } + + // 准备路径数据 + val points = combinedData.mapIndexed { index, value -> + val x = padding + (chartWidth / (combinedData.size - 1)) * index + val y = chartBottom - (value * chartHeight) + Offset(x, y) + } + + // 绘制垂直虚线和时间标签 + val timeLabels = listOf("00:00", "6:00", "12:00", "18:00", "24:00") + val timeHours = listOf(0f, 6f, 12f, 18f, 24f) + val timeTextStyle = TextStyle( + color = Color.White.copy(alpha = 0.7f), + fontSize = 12.sp + ) + + timeLabels.forEachIndexed { index, label -> + // 计算对应时间点的曲线Y坐标 + val timeHour = timeHours[index] + val dataIndex = (timeHour * combinedData.size / 24f).toInt() + .coerceIn(0, combinedData.size - 1) + + // 使用与曲线数据点相同的x坐标计算方式 + val x = if (points.isNotEmpty()) { + points[dataIndex].x + } else { + padding + (chartWidth / (timeLabels.size - 1)) * index + } + + val curveY = if (points.isNotEmpty()) { + points[dataIndex].y + } else { + chartBottom - chartHeight * 0.5f // 默认中间位置 + } + + // 绘制垂直虚线(从曲线位置到时间轴) + drawDashedLine( + start = Offset(x, curveY), + end = Offset(x, chartBottom), + color = Color.White.copy(alpha = 0.2f), + dashWidth = 4f, + gapWidth = 6f + ) + + // 绘制时间标签 + val textLayoutResult = textMeasurer.measure(label, timeTextStyle) + drawText( + textLayoutResult = textLayoutResult, + topLeft = Offset( + x - textLayoutResult.size.width / 2, + chartBottom + 12.dp.toPx() + ) + ) + } + + // 绘制曲线 + if (points.isNotEmpty()) { + // 分离实际数据点和预测数据点 + val actualPoints = points.take(actualDataSize) + val predictedPoints = if (actualDataSize < points.size) { + // 从实际数据的最后一个点开始,包含预测数据 + points.drop(actualDataSize - 1) + } else { + emptyList() + } + + // 绘制实际数据曲线(彩色渐变) + if (actualPoints.isNotEmpty()) { + val gradientColors = listOf( + Color(0xFF00C851), // 亮绿色 + Color(0xFF66BB6A), // 中绿色 + Color(0xFFFFEB3B), // 黄色 + Color(0xFFFF9800), // 橙色 + Color(0xFFFF5722) // 深橙色 + ) + + val gradientBrush = Brush.linearGradient( + colors = gradientColors, + start = Offset(actualPoints.first().x, 0f), + end = Offset(actualPoints.last().x, 0f) + ) + + drawSmoothCurve( + points = actualPoints, + brush = gradientBrush, + strokeWidth = 4.dp.toPx() + ) + } + + // 绘制预测数据曲线(白色) + if (predictedPoints.isNotEmpty()) { + drawSmoothCurve( + points = predictedPoints, + color = Color.White.copy(alpha = 0.9f), + strokeWidth = 3.dp.toPx() + ) + } + + // 绘制分界点的白色亮点(在实际数据的最后一个点) + if (actualPoints.isNotEmpty()) { + val dividerPoint = actualPoints.last() + + // 外层光晕 + drawCircle( + color = Color.White.copy(alpha = 0.3f), + radius = 16.dp.toPx(), + center = dividerPoint + ) + // 中层光晕 + drawCircle( + color = Color.White.copy(alpha = 0.6f), + radius = 10.dp.toPx(), + center = dividerPoint + ) + // 核心亮点 + drawCircle( + color = Color.White, + radius = 6.dp.toPx(), + center = dividerPoint + ) + } + } +} + +private fun DrawScope.drawDashedLine( + start: Offset, + end: Offset, + color: Color, + dashWidth: Float = 8f, + gapWidth: Float = 8f +) { + val totalDistance = (end - start).getDistance() + val dashCount = (totalDistance / (dashWidth + gapWidth)).toInt() + + repeat(dashCount) { index -> + val startRatio = index * (dashWidth + gapWidth) / totalDistance + val endRatio = (index * (dashWidth + gapWidth) + dashWidth) / totalDistance + + if (endRatio <= 1f) { + val dashStart = start + (end - start) * startRatio + val dashEnd = start + (end - start) * endRatio.coerceAtMost(1f) + + drawLine( + color = color, + start = dashStart, + end = dashEnd, + strokeWidth = 1.dp.toPx() + ) + } + } +} + +private fun DrawScope.drawSmoothCurve( + points: List, + brush: Brush? = null, + color: Color = Color.Transparent, + strokeWidth: Float = 4f, + isDashed: Boolean = false +) { + if (points.size < 2) return + + val path = Path() + path.moveTo(points.first().x, points.first().y) + + // 使用三次贝塞尔曲线创建更平滑的路径 + for (i in 1 until points.size) { + val currentPoint = points[i] + val previousPoint = points[i - 1] + + // 计算控制点来创建平滑曲线 + val controlPointDistance = (currentPoint.x - previousPoint.x) * 0.3f + + val control1 = Offset( + previousPoint.x + controlPointDistance, + previousPoint.y + ) + val control2 = Offset( + currentPoint.x - controlPointDistance, + currentPoint.y + ) + + path.cubicTo( + control1.x, control1.y, + control2.x, control2.y, + currentPoint.x, currentPoint.y + ) + } + + val pathEffect = if (isDashed) { + PathEffect.dashPathEffect(floatArrayOf(12f, 8f)) + } else null + + if (brush != null) { + drawPath( + path = path, + brush = brush, + style = Stroke( + width = strokeWidth, + pathEffect = pathEffect, + cap = StrokeCap.Round, + join = StrokeJoin.Round + ) + ) + } else { + drawPath( + path = path, + color = color, + style = Stroke( + width = strokeWidth, + pathEffect = pathEffect, + cap = StrokeCap.Round, + join = StrokeJoin.Round + ) + ) + } +} + +// 生成插值数据 +private fun generateInterpolatedData(data: List, targetSize: Int): List { + if (data.isEmpty()) return List(targetSize) { 0.5f } + if (data.size == targetSize) return data + + return (0 until targetSize).map { index -> + val ratio = index.toFloat() / (targetSize - 1) + val sourceIndex = ratio * (data.size - 1) + val lowerIndex = sourceIndex.toInt().coerceIn(0, data.size - 1) + val upperIndex = (lowerIndex + 1).coerceIn(0, data.size - 1) + val fraction = sourceIndex - lowerIndex + + // 线性插值 + data[lowerIndex] * (1 - fraction) + data[upperIndex] * fraction + } +} + +// 生成示例实际数据(0-18小时,共18个点) +private fun generateSampleActualData(): List { + return (0..17).map { hour -> + val timeInHours = hour.toFloat() + + val baseValue = when { + timeInHours < 6f -> { + // 夜间:低水平,逐渐恢复 + 0.2f + (timeInHours / 6f) * 0.3f + } + + timeInHours < 12f -> { + // 上午:快速上升 + 0.5f + ((timeInHours - 6f) / 6f) * 0.4f + } + + else -> { + // 下午:逐渐下降 + 0.9f - ((timeInHours - 12f) / 6f) * 0.3f + } + } + + // 添加轻微波动 + val noise = sin(timeInHours * 0.5f).toFloat() * 0.05f + (baseValue + noise).coerceIn(0f, 1f) + } +} + +// 生成示例预测数据(18-24小时,共6个点) +private fun generateSamplePredictedData(): List { + return (18..23).map { hour -> + val timeInHours = hour.toFloat() + + // 傍晚到夜间的预测:继续下降 + val baseValue = 0.6f - ((timeInHours - 18f) / 6f) * 0.4f + + // 添加轻微波动 + val noise = cos(timeInHours * 0.3f).toFloat() * 0.03f + (baseValue + noise).coerceIn(0f, 1f) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/CircularProgressIndicator.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgress.kt similarity index 89% rename from shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/CircularProgressIndicator.kt rename to shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgress.kt index fe04503..118a9b8 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/CircularProgressIndicator.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgress.kt @@ -1,4 +1,4 @@ -package com.whitefish.ring.ui.home.state.components +package com.whitefish.ring.ui.components import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -8,6 +8,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke @@ -16,7 +18,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun CircularProgressIndicator( +fun CircularProgress( progress: Float, value: String, unit: String, @@ -53,7 +55,7 @@ fun CircularProgressIndicator( drawCircle( color = backgroundColor, radius = radius, - center = androidx.compose.ui.geometry.Offset(centerX, centerY), + center = Offset(centerX, centerY), style = Stroke(width = strokeWidth, cap = StrokeCap.Round) ) @@ -65,11 +67,11 @@ fun CircularProgressIndicator( sweepAngle = sweepAngle, useCenter = false, style = Stroke(width = strokeWidth, cap = StrokeCap.Round), - topLeft = androidx.compose.ui.geometry.Offset( + topLeft = Offset( centerX - radius, centerY - radius ), - size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2) + size = Size(radius * 2, radius * 2) ) } diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgressCard.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgressCard.kt new file mode 100644 index 0000000..6dffda7 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgressCard.kt @@ -0,0 +1,166 @@ +package com.whitefish.ring.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.math.cos +import kotlin.math.sin + +/** + * 圆弧进度卡片组件 + * @param currentMinutes 当前分钟数 + * @param totalMinutes 总分钟数 + * @param completionTime 完成时间描述 + * @param modifier 修饰符 + */ +@Composable +fun CircularProgressCard( + currentMinutes: Int, + totalMinutes: Int, + completionTime: String, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + contentAlignment = Alignment.Center + ) { + // 圆弧进度条 + CircularProgressArc( + progress = currentMinutes.toFloat() / totalMinutes.toFloat(), + modifier = Modifier.width(195.dp).height(97.5.dp) + ) + + // 中心文字 - 与圆弧底部对齐 + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.offset(y = 35.dp) // 向下偏移,与圆弧底部对齐 + ) { + Text( + text = currentMinutes.toString(), + style = MaterialTheme.typography.displayLarge.copy( + fontSize = 40.sp, + fontWeight = FontWeight.Bold + ), + color = Color(0xFF352764) + ) + } + } + + // 底部完成时间描述 - 紧贴圆弧下方 + Row( + modifier = Modifier.padding(top = 10.dp) + ) { + Text( + text = completionTime, + style = MaterialTheme.typography.bodyMedium.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ), + color = Color(0xFF394298), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.width(8.dp)) + + // 问号图标 + Box( + modifier = Modifier + .size(16.dp) + .background( + color = Color.Transparent, + shape = CircleShape + ) + .border( + width = 1.dp, + color = Color(0xFF9CA3AF), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "?", + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ), + color = Color(0xFF9CA3AF) + ) + } + } + } +} + +/** + * 圆弧进度组件 + * @param progress 进度值 (0.0 到 1.0) + * @param modifier 修饰符 + */ +@Composable +private fun CircularProgressArc( + progress: Float, + modifier: Modifier = Modifier +) { + Canvas(modifier = modifier) { + + val strokeWidth = 12.dp.toPx() + val radius = (size.width - strokeWidth) / 2 + val center = androidx.compose.ui.geometry.Offset( + x = size.width / 2, + y = size.height - strokeWidth / 2 // 调整中心点,让半圆底部对齐 + ) + + // 背景圆弧 (浅灰色) - 180度半圆 + drawArc( + color = Color(0xFFE5E7EB), + startAngle = 180f, // 从左侧开始 + sweepAngle = 180f, // 180度的半圆 + useCenter = false, + topLeft = androidx.compose.ui.geometry.Offset( + x = center.x - radius, + y = center.y - radius + ), + size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2), + style = Stroke( + width = strokeWidth, + cap = StrokeCap.Round + ) + ) + + // 进度圆弧 (紫色渐变) + val progressAngle = 180f * progress.coerceIn(0f, 1f) + drawArc( + color = Color(0xFF352764), + startAngle = 180f, + sweepAngle = progressAngle, + useCenter = false, + topLeft = androidx.compose.ui.geometry.Offset( + x = center.x - radius, + y = center.y - radius + ), + size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2), + style = Stroke( + width = strokeWidth, + cap = StrokeCap.Round + ) + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularScoreChart.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularScoreChart.kt new file mode 100644 index 0000000..e000319 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularScoreChart.kt @@ -0,0 +1,74 @@ +package com.whitefish.ring.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun CircularScoreChart(score: Int,scoreText: String,title:String,scoreTextSize: TextUnit,bottomText: String = "") { + Box(contentAlignment = Alignment.BottomCenter) { + Canvas(modifier = Modifier.size(250.dp, 125.dp)) { + val strokeWidth = 12.dp.toPx() + val radius = (size.width - strokeWidth) / 2 + val center = Offset(size.width / 2, size.height) + + // 背景半圆弧 + drawArc( + color = Color(0xFFE0E0E0), + startAngle = 180f, + sweepAngle = 180f, + useCenter = false, + style = Stroke(strokeWidth, cap = StrokeCap.Round), + topLeft = Offset(center.x - radius, center.y - radius), + size = Size(radius * 2, radius * 2) + ) + + // 进度半圆弧 + val sweepAngle = (score / 100f) * 180f + drawArc( + color = Color(0xFF5E35B1), + startAngle = 180f, + sweepAngle = sweepAngle, + useCenter = false, + style = Stroke(strokeWidth, cap = StrokeCap.Round), + topLeft = Offset(center.x - radius, center.y - radius), + size = Size(radius * 2, radius * 2) + ) + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(bottom = 8.dp) + ) { + Text( + text = scoreText, + fontSize = scoreTextSize, + fontWeight = FontWeight.Bold, + color = Color(0xFF352764) + ) + Text( + text = title, + fontSize = 14.sp, + color = Color(0xFF352764) + ) + } + + + } +} diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/TopNavigationBar.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/TopNavigationBar.kt new file mode 100644 index 0000000..961f046 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/TopNavigationBar.kt @@ -0,0 +1,108 @@ +package com.whitefish.ring.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * 顶部导航栏组件 + * @param title 标题文字 + * @param month 月 + * @param day 总天数 + * @param onNavigateClick 右侧箭头点击事件 + * @param modifier 修饰符 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopNavigationBar( + title: String, + month: Int, + day: Int, + onNavigateClick: () -> Unit = {}, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + // 左侧日期椭圆 + DateOval( + month = month, + day = day, + modifier = Modifier + .width(30.dp) + .height(53.dp) + ) + + // 中间标题 + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy( + fontSize = 22.sp, + fontWeight = FontWeight.SemiBold + ), + color = Color(0xFF394298), + modifier = Modifier + .weight(1f) + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center + ) + + // 右侧箭头 + IconButton( + onClick = onNavigateClick, + modifier = Modifier.size(48.dp) + ) { +// Icon( +// imageVector = Icons.AutoMirrored.Filled.ArrowForward, +// contentDescription = "导航", +// tint = MaterialTheme.colorScheme.primary, +// modifier = Modifier.size(24.dp) +// ) + } + } +} + +/** + * 日期椭圆组件 + * @param currentDay 当前日期 + * @param totalDays 总天数 + * @param modifier 修饰符 + */ +@Composable +private fun DateOval( + month: Int, + day: Int, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(24.dp)) + .background( + color = Color(0xFF2D2D3A) + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "$month/\n$day", + style = MaterialTheme.typography.titleMedium.copy( + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ), + color = Color.White + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/exercise/ExerciseScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/exercise/ExerciseScreen.kt index 6bf56b2..6c62370 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/exercise/ExerciseScreen.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/exercise/ExerciseScreen.kt @@ -19,8 +19,11 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.whitefish.ring.ui.components.CircularScoreChart import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview @@ -334,7 +337,7 @@ fun ComprehensiveScoreSection() { .height(140.dp), contentAlignment = Alignment.Center ) { - CircularScoreChart(score = 75) + CircularScoreChart(score = 75,"75","运动得分",36.sp) } Spacer(modifier = Modifier.height(20.dp)) @@ -371,57 +374,6 @@ fun ComprehensiveScoreSection() { } } -@Composable -fun CircularScoreChart(score: Int) { - Box(contentAlignment = Alignment.BottomCenter) { - Canvas(modifier = Modifier.size(250.dp, 125.dp)) { - val strokeWidth = 12.dp.toPx() - val radius = (size.width - strokeWidth) / 2 - val center = Offset(size.width / 2, size.height) - - // 背景半圆弧 - drawArc( - color = Color(0xFFE0E0E0), - startAngle = 180f, - sweepAngle = 180f, - useCenter = false, - style = Stroke(strokeWidth), - topLeft = Offset(center.x - radius, center.y - radius), - size = Size(radius * 2, radius * 2) - ) - - // 进度半圆弧 - val sweepAngle = (score / 100f) * 180f - drawArc( - color = Color(0xFF5E35B1), - startAngle = 180f, - sweepAngle = sweepAngle, - useCenter = false, - style = Stroke(strokeWidth, cap = StrokeCap.Round), - topLeft = Offset(center.x - radius, center.y - radius), - size = Size(radius * 2, radius * 2) - ) - } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(bottom = 8.dp) - ) { - Text( - text = score.toString(), - fontSize = 36.sp, - fontWeight = FontWeight.Bold, - color = Color(0xFF333333) - ) - Text( - text = "运动得分", - fontSize = 14.sp, - color = Color(0xFF666666) - ) - } - } -} - @Composable fun ScoreIndicator( label: String, diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryScreen.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryScreen.kt index 83cf3a9..738f6dc 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryScreen.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryScreen.kt @@ -2,132 +2,68 @@ package com.whitefish.ring.ui.home.recovery import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush 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 androidx.lifecycle.viewmodel.compose.viewModel -import com.whitefish.app.ui.home.recovery.RecoveryViewModel +import com.whitefish.ring.ui.chart.RecoveryChartWithTimeLabels +import com.whitefish.ring.ui.components.TopNavigationBar +import com.whitefish.ring.ui.components.systemBarsPadding +import com.whitefish.ring.ui.components.CircularProgressCard import org.jetbrains.compose.ui.tooling.preview.Preview -import kotlin.invoke @Composable fun RecoveryScreen( modifier: Modifier = Modifier, viewModel: RecoveryViewModel = viewModel { RecoveryViewModel() } ) { - val uiState by viewModel.uiState.collectAsState() - - LazyColumn( + Column( modifier = modifier .fillMaxSize() - .background(Color(0xFF1A1A1A)) - .padding(horizontal = 16.dp), - contentPadding = PaddingValues(vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(uiState.recoveryData) { recovery -> - RecoveryCard(recovery = recovery) - } - } -} - -@Composable -private fun RecoveryCard( - recovery: RecoveryData, - modifier: Modifier = Modifier -) { - Card( - modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = Color(0xFF2A2A2A) - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = recovery.title, - color = Color.White, - fontSize = 16.sp, - fontWeight = FontWeight.Medium - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = recovery.description, - color = Color.Gray, - fontSize = 14.sp - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - RecoveryMetric( - label = "恢复指数", - value = recovery.recoveryIndex.toString() - ) - RecoveryMetric( - label = "建议休息", - value = recovery.restTime + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFF6B73FF).copy(alpha = 0.1f), + Color(0xFF9DD5EA).copy(alpha = 0.05f), + MaterialTheme.colorScheme.background + ) ) - RecoveryMetric( - label = "状态", - value = recovery.status - ) - } - } - } -} - -@Composable -private fun RecoveryMetric( - label: String, - value: String -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally + ) + .systemBarsPadding() ) { - Text( - text = value, - color = Color.White, - fontSize = 14.sp, - fontWeight = FontWeight.Bold + // 顶部导航栏 + TopNavigationBar( + title = "恢复得分", + month = 7, + day = 14, + onNavigateClick = { + // TODO: 实现导航逻辑 + } ) - Text( - text = label, - color = Color.Gray, - fontSize = 12.sp + + // 圆弧进度卡片 + CircularProgressCard( + currentMinutes = 75, + totalMinutes = 120, // 假设总时长为120分钟 + completionTime = "预计7/15日18:36后完全恢复" ) + + RecoveryChartWithTimeLabels( + modifier = Modifier.fillMaxWidth().height(300.dp), + actualData = arrayListOf(1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f), + predictedData = arrayListOf() + ) + } } -data class RecoveryData( - val title: String, - val description: String, - val recoveryIndex: Int, - val restTime: String, - val status: String -) - -@Preview @Composable -private fun RecoveryScreenPreview() { - MaterialTheme { - RecoveryScreen() - } -} \ No newline at end of file +@Preview +fun RecoveryPreview() { + RecoveryScreen( + modifier = Modifier.fillMaxSize() + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryViewModel.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryViewModel.kt index e5bbe7f..29622ed 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryViewModel.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryViewModel.kt @@ -1,57 +1,9 @@ -package com.whitefish.app.ui.home.recovery +package com.whitefish.ring.ui.home.recovery import androidx.lifecycle.ViewModel -import com.whitefish.ring.ui.home.recovery.RecoveryData -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -data class RecoveryUiState( - val recoveryData: List = emptyList(), - val isLoading: Boolean = false -) class RecoveryViewModel : ViewModel() { - - private val _uiState = MutableStateFlow(RecoveryUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - init { - loadRecoveryData() - } - - private fun loadRecoveryData() { - // 模拟加载恢复数据 - val mockData = listOf( - RecoveryData( - title = "今日恢复状态", - description = "基于昨晚睡眠质量和心率变异性分析", - recoveryIndex = 85, - restTime = "6小时", - status = "良好" - ), - RecoveryData( - title = "压力水平", - description = "当前压力指数偏低,适合进行中等强度运动", - recoveryIndex = 72, - restTime = "4小时", - status = "正常" - ), - RecoveryData( - title = "肌肉恢复", - description = "上次运动后肌肉恢复情况", - recoveryIndex = 90, - restTime = "2小时", - status = "优秀" - ) - ) - - _uiState.value = _uiState.value.copy(recoveryData = mockData) - } - - fun refresh() { - _uiState.value = _uiState.value.copy(isLoading = true) - loadRecoveryData() - _uiState.value = _uiState.value.copy(isLoading = false) - } + + } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/ExerciseGoalCard.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/ExerciseGoalCard.kt index 5d857c5..3823092 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/ExerciseGoalCard.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/ExerciseGoalCard.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.whitefish.ring.ui.components.CircularProgress import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview import ring.shared.generated.resources.Res @@ -67,7 +68,7 @@ fun ExerciseGoalCard( Spacer(modifier = Modifier.height(16.dp)) // 圆形进度条 - CircularProgressIndicator( + CircularProgress( progress = progress, value = data.caloriesBurned.toString(), unit = "千卡", diff --git a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/RecoveryScoreCard.kt b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/RecoveryScoreCard.kt index 3eaea17..052496f 100644 --- a/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/RecoveryScoreCard.kt +++ b/shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/RecoveryScoreCard.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.whitefish.ring.ui.components.CircularProgress import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.ui.tooling.preview.Preview import ring.shared.generated.resources.Res @@ -69,7 +70,7 @@ fun RecoveryScoreCard(data: RecoveryScoreData, modifier: Modifier = Modifier){ Spacer(modifier = Modifier.height(16.dp)) // 圆形进度条 - CircularProgressIndicator( + CircularProgress( progress = data.score.toFloat(), value = data.score.toString(), unit = "分", diff --git a/shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt b/shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt index 2536808..9856b09 100644 --- a/shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt +++ b/shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt @@ -71,7 +71,7 @@ class DeviceManager : IDeviceManager() { scope.launch { val address = mac() - if (autoConnect && !address.isNullOrEmpty() && !autoConnecting) { + if (enableAutoConnect && !address.isNullOrEmpty() && !autoConnecting) { autoConnecting = true connect(address) Napier.i { "Auto connect:${address}" } @@ -231,6 +231,6 @@ class DeviceManager : IDeviceManager() { } override fun setAutoConnect(enable: Boolean) { - autoConnect = enable + enableAutoConnect = enable } } \ No newline at end of file