fix: 修正终审发现的详情页配色、空态与血压卡可点击等问题

- 详情页柱状图改用指标语义色(新增 MetricType→色板映射),此前恒定绿色
- 详情页无数据时渲染空态文案,不再画全 0 柱状图与三行 0.0
- 首页「未开启」指标卡不再可点击,与详情页内容保持一致
- 压力卡单位位显示等级文案(如「34 正常」),接上原先无人调用的 stressLevelLabelRes
- NavHost 补 consumeWindowInsets,避免详情页 TopAppBar 重复叠加状态栏内边距
- README 注明 installDebug 需要设备/模拟器

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-10 13:18:51 +08:00
co-authored by Claude Code
parent 9c233e5750
commit 5fe0d4a420
6 changed files with 71 additions and 7 deletions
+4 -1
View File
@@ -24,8 +24,11 @@
```bash
./gradlew assembleDebug # 产物在 app/build/outputs/apk/debug/
./gradlew installDebug # 安装到已连接设备
./gradlew installDebug # 安装到已连接设备(需接好设备或已启动模拟器)
./gradlew test # 单元测试
```
> `installDebug` 需要一台已连接的设备或正在运行的模拟器。M1 使用的机器两者都没有,
> 因此本机实际可执行的是 `./gradlew assembleDebug` 与 `./gradlew :app:testDebugUnitTest`。
要求:JDK 17、Android SDK(platform 36 / build-tools 36.1.0)。
@@ -30,7 +30,8 @@ import com.placeholder.ringhealth.core.designsystem.TextSecondary
* @param value 主数值文本。
* @param unit 单位文本,如「步」「次/分」。
* @param modifier 外部修饰符。
* @param onClick 点击回调,默认空实现(配合 HealthCard 的整卡可点击)。
* @param onClick 点击回调;为 null 表示卡片不可点击 —— 不挂 clickable,因此既没有水波纹
* 也不会触发导航,用于「未开启」这类没有数值的指标卡(有值的卡才可跳转详情)。
*/
@Composable
fun MetricTile(
@@ -40,7 +41,7 @@ fun MetricTile(
value: String,
unit: String,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onClick: (() -> Unit)? = null,
) {
HealthCard(modifier = modifier, onClick = onClick) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -1,5 +1,6 @@
package com.placeholder.ringhealth.core.navigation
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FitnessCenter
@@ -90,7 +91,12 @@ fun RingHealthApp(navController: NavHostController = rememberNavController()) {
NavHost(
navController = navController,
startDestination = Routes.HOME,
modifier = Modifier.padding(innerPadding),
// consumeWindowInsets 必须跟着 padding 一起写:外层 Scaffold 没有 topBar,
// 它的 innerPadding.top 就是状态栏高度,若只 padding 不消费,这个 inset 会继续
// 往下传,详情页内层 Scaffold 的 TopAppBar 会再加一次状态栏内边距 ——
// AppBar 被推低约两个状态栏,上方露出一条背景色带,与四个 Tab 页明显不同。
// 消费后内层拿到的是已扣减的 inset,两个方向都安全(若外层早已消费,这里是 no-op)。
modifier = Modifier.padding(innerPadding).consumeWindowInsets(innerPadding),
) {
composable(Routes.HOME) {
HomeScreen(onOpenDetail = { metric -> navController.navigate(Routes.detail(metric)) })
@@ -37,7 +37,12 @@ import com.placeholder.ringhealth.R
import com.placeholder.ringhealth.core.common.formatClock
import com.placeholder.ringhealth.core.common.formatDecimal
import com.placeholder.ringhealth.core.designsystem.MetricBlue
import com.placeholder.ringhealth.core.designsystem.MetricCyan
import com.placeholder.ringhealth.core.designsystem.MetricGreen
import com.placeholder.ringhealth.core.designsystem.MetricOrange
import com.placeholder.ringhealth.core.designsystem.MetricPurple
import com.placeholder.ringhealth.core.designsystem.MetricRed
import com.placeholder.ringhealth.core.designsystem.MetricYellow
import com.placeholder.ringhealth.core.designsystem.RingHealthTheme
import com.placeholder.ringhealth.core.designsystem.TextSecondary
import com.placeholder.ringhealth.core.designsystem.component.HealthCard
@@ -75,6 +80,8 @@ fun MetricDetailScreen(
/**
* 无状态指标详情内容:范围切换 + 柱状趋势图 + 统计摘要(+ 睡眠分期)。
*
* 范围切换常驻;当前范围没有数据点时,只把「趋势图 + 统计摘要」换成一行空态文案。
*
* 标题取自 `state.metric` 而不是外面传进来的 `metric` —— 无状态层只有 `state` 一个数据来源,
* 免得出现「标题写心率、数据画睡眠」这种两者不一致的可能。
*/
@@ -118,13 +125,35 @@ private fun MetricDetailContent(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
// 范围切换常驻,空数据时反而更要留着:切到另一个范围是用户看到「暂无数据」后的
// 第一反应,chips 也是判断「哪个范围是空的」的唯一线索。它属于页头,不属于数据区。
item { RangeSelector(current = state.range, onSelect = onSelectRange) }
// 只有图与统计被替换为空态。判空必须在 loading 之后:Mock 有约 200ms 延迟,
// 加载期间 points 也是空的,先判空会在数据到达前闪一下空态(loading 分支已提前 return)。
if (state.points.isEmpty()) {
// 不再渲染「全 0 高度的柱状图 + 三行 0.0」,那看着像页面坏了而不是「没有数据」。
item {
Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 48.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.detail_empty),
style = MaterialTheme.typography.bodyMedium,
color = TextSecondary,
)
}
}
return@LazyColumn
}
item {
HealthCard {
SectionHeader(title = stringResource(R.string.detail_trend))
MetricBarChart(
values = state.points.map { it.value.toFloat() },
color = MetricGreen,
color = metricTint(state.metric),
)
}
}
@@ -228,6 +257,23 @@ private fun metricTitleRes(type: MetricType): Int = when (type) {
MetricType.BLOOD_PRESSURE -> R.string.metric_blood_pressure
}
/**
* 指标类型 → 语义色。
*
* 与 `HomeScreen` 里各指标卡的 tint 一一对应(步数=绿、心率=红、睡眠=蓝、血氧=青、
* 压力=紫、体温=橙、血压=黄)。色板的唯一来源是设计文档 §指标色板与
* `core/designsystem/Color.kt`,本函数只做映射,不在页面里另造颜色。
*/
private fun metricTint(type: MetricType): Color = when (type) {
MetricType.STEP -> MetricGreen
MetricType.HEART_RATE -> MetricRed
MetricType.SLEEP -> MetricBlue
MetricType.BLOOD_OXYGEN -> MetricCyan
MetricType.STRESS -> MetricPurple
MetricType.TEMPERATURE -> MetricOrange
MetricType.BLOOD_PRESSURE -> MetricYellow
}
/** 周/月切换。 */
@Composable
private fun RangeSelector(
@@ -42,6 +42,7 @@ import com.placeholder.ringhealth.core.common.formatCalories
import com.placeholder.ringhealth.core.common.formatDistance
import com.placeholder.ringhealth.core.common.formatSleepDuration
import com.placeholder.ringhealth.core.common.formatSteps
import com.placeholder.ringhealth.core.common.stressLevelLabelRes
import com.placeholder.ringhealth.core.designsystem.MetricBlue
import com.placeholder.ringhealth.core.designsystem.MetricCyan
import com.placeholder.ringhealth.core.designsystem.MetricGreen
@@ -109,13 +110,16 @@ private fun HomeContent(
item { StepsCard(summary = summary, onClick = { onOpenDetail(MetricType.STEP) }) }
item { SectionHeader(title = stringResource(R.string.home_metrics)) }
items(cards) { card ->
// 没有数值(未开启)的指标卡不可点击:传 null 后 MetricTile/HealthCard 不挂 clickable,
// 无跳转也无水波纹。否则首页显示「未开启」、点进去却是一张完整图表,两页自相矛盾。
val openDetail = card.value?.let { { onOpenDetail(card.type) } }
MetricTile(
icon = card.icon,
iconTint = card.tint,
title = card.title,
value = card.value ?: notEnabledText,
unit = if (card.value == null) "" else card.unit,
onClick = { onOpenDetail(card.type) },
onClick = openDetail,
)
}
}
@@ -239,7 +243,10 @@ private fun buildMetricCards(summary: HealthSummary): List<MetricCardSpec> = lis
tint = MetricPurple,
title = stringResource(R.string.metric_stress),
value = summary.stress?.toString(),
unit = "",
// 单位位改放压力等级文案(数值落在哪个区间就是放松/正常/中等/偏高),
// 于是卡片读作「34 正常」而不是一个孤零零的数字。无值时留空(上面的 value 分支
// 会整卡退化成「未开启」,此时单位本来也会被清空)。
unit = summary.stress?.let { stringResource(stressLevelLabelRes(it)) } ?: "",
),
MetricCardSpec(
type = MetricType.TEMPERATURE,
+1
View File
@@ -71,6 +71,7 @@
<string name="detail_max">最高</string>
<string name="detail_min">最低</string>
<string name="detail_trend">趋势</string>
<string name="detail_empty">暂无数据</string>
<string name="detail_sleep_stages">睡眠分期</string>
<string name="detail_sleep_deep">深睡</string>
<string name="detail_sleep_light">浅睡</string>