Browse Source

feat: recovery

dev_ios
AnranYus 3 weeks ago
parent
commit
5aa9860208
  1. 4
      shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt
  2. 9
      shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt
  3. BIN
      shared/src/commonMain/composeResources/drawable/bg_recovery_chart.png
  4. 1
      shared/src/commonMain/kotlin/com/whitefish/ring/App.kt
  5. 2
      shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt
  6. 284
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChart.kt
  7. 190
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartDemo.kt
  8. 432
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/chart/RecoveryChartWithTimeLabels.kt
  9. 12
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgress.kt
  10. 166
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularProgressCard.kt
  11. 74
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/CircularScoreChart.kt
  12. 108
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/components/TopNavigationBar.kt
  13. 56
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/exercise/ExerciseScreen.kt
  14. 138
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryScreen.kt
  15. 50
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/recovery/RecoveryViewModel.kt
  16. 3
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/ExerciseGoalCard.kt
  17. 3
      shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/RecoveryScoreCard.kt
  18. 4
      shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt

4
shared/src/androidMain/kotlin/com/whitefish/ring/DeviceManager.kt

@ -208,6 +208,10 @@ class DeviceManager() : IDeviceManager(), OnBleConnectionListener, OnSleepDataLo
return app.bleManager.connectedDevice?.address return app.bleManager.connectedDevice?.address
} }
override fun setAutoConnect(enable: Boolean) {
enableAutoConnect = enable
}
override fun startScan() { override fun startScan() {
val bluetoothAdapter = val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter

9
shared/src/androidMain/kotlin/com/whitefish/ring/data/DeviceDataProvider.android.kt

@ -1,6 +1,7 @@
package com.whitefish.ring.data package com.whitefish.ring.data
import com.whitefish.ring.bean.ui.HeartRate import com.whitefish.ring.bean.ui.HeartRate
import com.whitefish.ring.bean.ui.SleepState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -27,3 +28,11 @@ actual suspend fun getHeartRate(
} }
} }
} }
actual suspend fun getSleep(
start: Long,
end: Long
): SleepState {
//todo android sleep data
return SleepState(0,emptyList())
}

BIN
shared/src/commonMain/composeResources/drawable/bg_recovery_chart.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

1
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.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.whitefish.ring.device.IDeviceManager 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.guide.GuideNavigationScreen
import com.whitefish.ring.ui.home.HomeScreen import com.whitefish.ring.ui.home.HomeScreen
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview

2
shared/src/commonMain/kotlin/com/whitefish/ring/device/IDeviceManager.kt

@ -14,7 +14,7 @@ abstract class IDeviceManager {
private val bleStateListeners = arrayListOf<(Int) -> Unit>() private val bleStateListeners = arrayListOf<(Int) -> Unit>()
protected var onResetDeviceWhenBind:()-> Unit = {} protected var onResetDeviceWhenBind:()-> Unit = {}
protected var autoConnect = false protected var enableAutoConnect = false
fun bleStateListeners() = bleStateListeners fun bleStateListeners() = bleStateListeners

284
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<Float> = 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<Float>,
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<Offset>,
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<Float> {
// 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)
}
}

190
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<Float> {
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<Float> {
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)
}
}

432
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<Float> = generateSampleActualData(), // 实际数据(彩色曲线)
predictedData: List<Float> = 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<Float>,
predictedData: List<Float>,
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<Offset>,
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<Float>, targetSize: Int): List<Float> {
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<Float> {
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<Float> {
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)
}
}

12
shared/src/commonMain/kotlin/com/whitefish/ring/ui/home/state/components/CircularProgressIndicator.kt → 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.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@ -8,6 +8,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
@ -16,7 +18,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@Composable @Composable
fun CircularProgressIndicator( fun CircularProgress(
progress: Float, progress: Float,
value: String, value: String,
unit: String, unit: String,
@ -53,7 +55,7 @@ fun CircularProgressIndicator(
drawCircle( drawCircle(
color = backgroundColor, color = backgroundColor,
radius = radius, radius = radius,
center = androidx.compose.ui.geometry.Offset(centerX, centerY), center = Offset(centerX, centerY),
style = Stroke(width = strokeWidth, cap = StrokeCap.Round) style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
) )
@ -65,11 +67,11 @@ fun CircularProgressIndicator(
sweepAngle = sweepAngle, sweepAngle = sweepAngle,
useCenter = false, useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round), style = Stroke(width = strokeWidth, cap = StrokeCap.Round),
topLeft = androidx.compose.ui.geometry.Offset( topLeft = Offset(
centerX - radius, centerX - radius,
centerY - radius centerY - radius
), ),
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2) size = Size(radius * 2, radius * 2)
) )
} }

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

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

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

56
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.graphics.drawscope.Stroke
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.whitefish.ring.ui.components.CircularScoreChart
import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
@ -334,7 +337,7 @@ fun ComprehensiveScoreSection() {
.height(140.dp), .height(140.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
CircularScoreChart(score = 75) CircularScoreChart(score = 75,"75","运动得分",36.sp)
} }
Spacer(modifier = Modifier.height(20.dp)) 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 @Composable
fun ScoreIndicator( fun ScoreIndicator(
label: String, label: String,

138
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.background
import androidx.compose.foundation.layout.* 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel 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 org.jetbrains.compose.ui.tooling.preview.Preview
import kotlin.invoke
@Composable @Composable
fun RecoveryScreen( fun RecoveryScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: RecoveryViewModel = viewModel { RecoveryViewModel() } viewModel: RecoveryViewModel = viewModel { RecoveryViewModel() }
) { ) {
val uiState by viewModel.uiState.collectAsState() Column(
LazyColumn(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(Color(0xFF1A1A1A)) .background(
.padding(horizontal = 16.dp), brush = Brush.verticalGradient(
contentPadding = PaddingValues(vertical = 16.dp), colors = listOf(
verticalArrangement = Arrangement.spacedBy(12.dp) Color(0xFF6B73FF).copy(alpha = 0.1f),
) { Color(0xFF9DD5EA).copy(alpha = 0.05f),
items(uiState.recoveryData) { recovery -> MaterialTheme.colorScheme.background
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
) )
.systemBarsPadding()
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) { ) {
RecoveryMetric( // 顶部导航栏
label = "恢复指数", TopNavigationBar(
value = recovery.recoveryIndex.toString() title = "恢复得分",
) month = 7,
RecoveryMetric( day = 14,
label = "建议休息", onNavigateClick = {
value = recovery.restTime // TODO: 实现导航逻辑
)
RecoveryMetric(
label = "状态",
value = recovery.status
)
}
}
}
} }
)
@Composable // 圆弧进度卡片
private fun RecoveryMetric( CircularProgressCard(
label: String, currentMinutes = 75,
value: String totalMinutes = 120, // 假设总时长为120分钟
) { completionTime = "预计7/15日18:36后完全恢复"
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = value,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
) )
Text(
text = label, RecoveryChartWithTimeLabels(
color = Color.Gray, modifier = Modifier.fillMaxWidth().height(300.dp),
fontSize = 12.sp 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 @Composable
private fun RecoveryScreenPreview() { @Preview
MaterialTheme { fun RecoveryPreview() {
RecoveryScreen() RecoveryScreen(
} modifier = Modifier.fillMaxSize()
)
} }

50
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 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<RecoveryData> = emptyList(),
val isLoading: Boolean = false
)
class RecoveryViewModel : ViewModel() { class RecoveryViewModel : ViewModel() {
private val _uiState = MutableStateFlow(RecoveryUiState())
val uiState: StateFlow<RecoveryUiState> = _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)
}
} }

3
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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.whitefish.ring.ui.components.CircularProgress
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import ring.shared.generated.resources.Res import ring.shared.generated.resources.Res
@ -67,7 +68,7 @@ fun ExerciseGoalCard(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 圆形进度条 // 圆形进度条
CircularProgressIndicator( CircularProgress(
progress = progress, progress = progress,
value = data.caloriesBurned.toString(), value = data.caloriesBurned.toString(),
unit = "千卡", unit = "千卡",

3
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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.whitefish.ring.ui.components.CircularProgress
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import ring.shared.generated.resources.Res import ring.shared.generated.resources.Res
@ -69,7 +70,7 @@ fun RecoveryScoreCard(data: RecoveryScoreData, modifier: Modifier = Modifier){
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// 圆形进度条 // 圆形进度条
CircularProgressIndicator( CircularProgress(
progress = data.score.toFloat(), progress = data.score.toFloat(),
value = data.score.toString(), value = data.score.toString(),
unit = "", unit = "",

4
shared/src/iosMain/kotlin/com/whitefish/ring/DeviceManager.kt

@ -71,7 +71,7 @@ class DeviceManager : IDeviceManager() {
scope.launch { scope.launch {
val address = mac() val address = mac()
if (autoConnect && !address.isNullOrEmpty() && !autoConnecting) { if (enableAutoConnect && !address.isNullOrEmpty() && !autoConnecting) {
autoConnecting = true autoConnecting = true
connect(address) connect(address)
Napier.i { "Auto connect:${address}" } Napier.i { "Auto connect:${address}" }
@ -231,6 +231,6 @@ class DeviceManager : IDeviceManager() {
} }
override fun setAutoConnect(enable: Boolean) { override fun setAutoConnect(enable: Boolean) {
autoConnect = enable enableAutoConnect = enable
} }
} }
Loading…
Cancel
Save