docs: 按 pre-flight 冲突扫描修订 M0+M1 计划(R1-R4)

This commit is contained in:
2026-09-10 09:07:56 +08:00
parent 9411ac14d1
commit 616bd1aea2
2 changed files with 265 additions and 167 deletions
+1
View File
@@ -0,0 +1 @@
.superpowers/
@@ -986,13 +986,14 @@ git add -A && git commit -m "feat: 新增通用 UI 组件(卡片/环形进度/
---
### Task 4: 领域模型 + 仓库接口 + 依赖容器
### Task 4: 领域模型 + 仓库接口
**Files:**
- Create: `domain/model/HealthModels.kt`, `domain/repository/Repositories.kt`
- Create: `AppContainer.kt`
- Modify: `RingHealthApplication.kt`(持有容器)
- Create: `AppViewModelProvider.kt`
> **范围说明(见 ledger Ruling R1 / R2)**:依赖容器 `AppContainer.kt` 与 `AppViewModelProvider.kt`
> **不在本任务创建**——前者引用 Task 6 才存在的 Mock 实现类,后者引用 Task 8 才存在的 `HomeViewModel`。
> 若在本任务创建,Task 4 到 Task 6 之间的任何构建或单测都会因未解析引用而无法编译。
**Interfaces:**
- Consumes: 无(纯 Kotlin 层)
@@ -1006,7 +1007,6 @@ git add -A && git commit -m "feat: 新增通用 UI 组件(卡片/环形进度/
- `data class DeviceFunctionSetting(id, title, subtitle, enabled)`
- `interface HealthDataRepository { observeTodaySummary(); observeMetricHistory(metric, range); observeWorkouts() }`
- `interface RingDeviceRepository { deviceState; functionSettings; refreshDeviceState(); setFunctionEnabled(id, enabled) }`
- `AppContainer { healthDataRepository; ringDeviceRepository }`、`RingHealthApplication.container`、`AppViewModelProvider.Factory`
- [ ] **Step 1: 写 `HealthModels.kt`**
@@ -1123,111 +1123,18 @@ interface RingDeviceRepository {
}
```
- [ ] **Step 3: 写 `AppContainer.kt` 与更新 `RingHealthApplication.kt`**
> 依赖 Mock 实现,需在 Task 6 完成后才能编译通过;本任务把 Mock 类的引用先写好,Step 6 的构建验证放在 Task 6 之后执行。
`AppContainer.kt`:
```kotlin
package com.placeholder.ringhealth
import com.placeholder.ringhealth.data.mock.MockHealthDataRepository
import com.placeholder.ringhealth.data.mock.MockRingDeviceRepository
import com.placeholder.ringhealth.domain.repository.HealthDataRepository
import com.placeholder.ringhealth.domain.repository.RingDeviceRepository
/**
* 手动依赖注入容器。M2 接入 SDK 时只需在这里把 Mock 实现换成真实实现。
*/
interface AppContainer {
val healthDataRepository: HealthDataRepository
val ringDeviceRepository: RingDeviceRepository
}
/** 默认容器:阶段一全部使用 Mock 数据。 */
class DefaultAppContainer : AppContainer {
override val healthDataRepository: HealthDataRepository = MockHealthDataRepository()
override val ringDeviceRepository: RingDeviceRepository = MockRingDeviceRepository()
}
```
`RingHealthApplication.kt`(替换 Task 1 的占位实现):
```kotlin
package com.placeholder.ringhealth
import android.app.Application
/**
* 应用入口,同时作为手动依赖注入的组合根。
*/
class RingHealthApplication : Application() {
/** 全局依赖容器。 */
lateinit var container: AppContainer
private set
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer()
}
}
```
- [ ] **Step 4: 写 `AppViewModelProvider.kt`**
```kotlin
package com.placeholder.ringhealth
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.placeholder.ringhealth.feature.device.DeviceViewModel
import com.placeholder.ringhealth.feature.home.HomeViewModel
import com.placeholder.ringhealth.feature.workout.WorkoutViewModel
/**
* ViewModel 工厂:把容器中的仓库注入各 ViewModel。
*/
object AppViewModelProvider {
val Factory = viewModelFactory {
initializer {
HomeViewModel(
healthDataRepository = ringHealthApplication().container.healthDataRepository,
ringDeviceRepository = ringHealthApplication().container.ringDeviceRepository,
)
}
initializer {
WorkoutViewModel(
healthDataRepository = ringHealthApplication().container.healthDataRepository,
)
}
initializer {
DeviceViewModel(
ringDeviceRepository = ringHealthApplication().container.ringDeviceRepository,
)
}
}
}
/** 从 CreationExtras 中取出 Application 实例。 */
fun CreationExtras.ringHealthApplication(): RingHealthApplication =
this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as RingHealthApplication
```
> 说明:`MetricDetailViewModel` 需要运行时参数(指标类型),使用独立的工厂,见 Task 12。
- [ ] **Step 5: 暂不构建(依赖 Mock 类尚未创建)**
跳到 Task 5、Task 6,完成 Mock 后再统一验证。
- [ ] **Step 6: Commit**
- [ ] **Step 3: 构建验证**
```bash
git add -A && git commit -m "feat: 新增领域模型、仓库接口与依赖容器"
cd /d/cherry/repos/ring-health-app && ./gradlew assembleDebug
```
Expected: `BUILD SUCCESSFUL`。本任务只产出纯 Kotlin 的 `domain/` 层,不引用任何尚未创建的类。
- [ ] **Step 4: Commit**
```bash
git add -A && git commit -m "feat: 新增领域模型与仓库接口"
```
---
@@ -1249,7 +1156,11 @@ git add -A && git commit -m "feat: 新增领域模型、仓库接口与依赖容
- `formatClock(timestamp: Long): String`
- `formatDate(timestamp: Long): String`
- `stressLevelLabel(stress: Int): String`
- `metricDisplayName(type: MetricType): String`
> **范围说明(见 ledger Ruling R3)**:本任务**不提供** `metricDisplayName(type)`。
> 指标中文名属于用户可见文案,Global Constraints 要求一律进 `strings.xml`;
> 把它硬编码在 Kotlin 里会与 Task 8 加入 `R.string.metric_*` 形成两份事实来源。
> 详情页标题改用资源 id 映射,见 Task 12。
- [ ] **Step 1: 写失败的测试**
@@ -1258,7 +1169,6 @@ git add -A && git commit -m "feat: 新增领域模型、仓库接口与依赖容
```kotlin
package com.placeholder.ringhealth.core.common
import com.placeholder.ringhealth.domain.model.MetricType
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -1303,17 +1213,6 @@ class FormattersTest {
assertEquals("中等", stressLevelLabel(65))
assertEquals("偏高", stressLevelLabel(90))
}
@Test
fun `metricDisplayName 返回中文指标名`() {
assertEquals("步数", metricDisplayName(MetricType.STEP))
assertEquals("心率", metricDisplayName(MetricType.HEART_RATE))
assertEquals("睡眠", metricDisplayName(MetricType.SLEEP))
assertEquals("血氧", metricDisplayName(MetricType.BLOOD_OXYGEN))
assertEquals("压力", metricDisplayName(MetricType.STRESS))
assertEquals("体温", metricDisplayName(MetricType.TEMPERATURE))
assertEquals("血压", metricDisplayName(MetricType.BLOOD_PRESSURE))
}
}
```
@@ -1332,7 +1231,6 @@ Expected: 编译失败或测试失败(`formatSleepDuration` 等未定义 / `Un
```kotlin
package com.placeholder.ringhealth.core.common
import com.placeholder.ringhealth.domain.model.MetricType
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -1383,17 +1281,6 @@ fun stressLevelLabel(stress: Int): String = when {
stress < 80 -> "中等"
else -> "偏高"
}
/** 指标类型 → 中文名称。 */
fun metricDisplayName(type: MetricType): String = when (type) {
MetricType.STEP -> "步数"
MetricType.HEART_RATE -> "心率"
MetricType.SLEEP -> "睡眠"
MetricType.BLOOD_OXYGEN -> "血氧"
MetricType.STRESS -> "压力"
MetricType.TEMPERATURE -> "体温"
MetricType.BLOOD_PRESSURE -> "血压"
}
```
- [ ] **Step 4: 运行测试,确认通过**
@@ -1402,7 +1289,7 @@ fun metricDisplayName(type: MetricType): String = when (type) {
cd /d/cherry/repos/ring-health-app && ./gradlew :app:testDebugUnitTest --tests "com.placeholder.ringhealth.core.common.FormattersTest"
```
Expected: `BUILD SUCCESSFUL`,7 个测试全部通过。
Expected: `BUILD SUCCESSFUL`,6 个测试全部通过。
- [ ] **Step 5: Commit**
@@ -1416,11 +1303,14 @@ git add -A && git commit -m "feat: 新增数值格式化工具(含单元测试
**Files:**
- Create: `data/mock/MockDataProvider.kt`, `MockHealthDataRepository.kt`, `MockRingDeviceRepository.kt`
- Create: `AppContainer.kt`(原属 Task 4,见 ledger Ruling R1)
- Modify: `RingHealthApplication.kt`(持有容器)
- Test: `app/src/test/java/com/placeholder/ringhealth/data/mock/MockDataProviderTest.kt`
**Interfaces:**
- Consumes: `domain/model/*`、`domain/repository/*`
- Produces:
- `interface AppContainer { healthDataRepository; ringDeviceRepository }`、`class DefaultAppContainer : AppContainer`、`RingHealthApplication.container`
- `class MockDataProvider(seed: Long = DEFAULT_SEED)`,方法:`todaySummary(): HealthSummary`、`metricHistory(metric: MetricType, range: DateRange): List<MetricPoint>`、`workouts(): List<WorkoutRecord>`、`deviceState(): RingDeviceState`、`defaultFunctionSettings(): List<DeviceFunctionSetting>`
- `class MockHealthDataRepository : HealthDataRepository`(每次发射前 `delay(200)`)
- `class MockRingDeviceRepository : RingDeviceRepository`(开关状态保存在内存 StateFlow)
@@ -1696,18 +1586,70 @@ class MockRingDeviceRepository(
}
```
- [ ] **Step 7: 全量单元测试 + 构建验证**
- [ ] **Step 7: 写 `AppContainer.kt` 与更新 `RingHealthApplication.kt`**
现在 Mock 实现已存在,可以安全创建容器(ledger Ruling R1)。
`AppContainer.kt`:
```kotlin
package com.placeholder.ringhealth
import com.placeholder.ringhealth.data.mock.MockHealthDataRepository
import com.placeholder.ringhealth.data.mock.MockRingDeviceRepository
import com.placeholder.ringhealth.domain.repository.HealthDataRepository
import com.placeholder.ringhealth.domain.repository.RingDeviceRepository
/**
* 手动依赖注入容器。M2 接入 SDK 时只需在这里把 Mock 实现换成真实实现。
*/
interface AppContainer {
val healthDataRepository: HealthDataRepository
val ringDeviceRepository: RingDeviceRepository
}
/** 默认容器:阶段一全部使用 Mock 数据。 */
class DefaultAppContainer : AppContainer {
override val healthDataRepository: HealthDataRepository = MockHealthDataRepository()
override val ringDeviceRepository: RingDeviceRepository = MockRingDeviceRepository()
}
```
`RingHealthApplication.kt`(替换 Task 1 的最小实现):
```kotlin
package com.placeholder.ringhealth
import android.app.Application
/**
* 应用入口,同时作为手动依赖注入的组合根。
*/
class RingHealthApplication : Application() {
/** 全局依赖容器。 */
lateinit var container: AppContainer
private set
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer()
}
}
```
- [ ] **Step 8: 全量单元测试 + 构建验证**
```bash
cd /d/cherry/repos/ring-health-app && ./gradlew :app:testDebugUnitTest && ./gradlew assembleDebug
```
Expected: 两个命令均 `BUILD SUCCESSFUL`(Task 4 的容器代码此时也应编译通过)。
Expected: 两个命令均 `BUILD SUCCESSFUL`(Step 7 的容器代码也在本次编译范围内)。
- [ ] **Step 8: Commit**
- [ ] **Step 9: Commit**
```bash
git add -A && git commit -m "feat: 新增 Mock 数据层(Provider + 两个仓库实现,含单元测试)"
git add -A && git commit -m "feat: 新增 Mock 数据层与依赖容器(含单元测试)"
```
---
@@ -2076,15 +2018,17 @@ git add -A && git commit -m "feat: 新增底部四 Tab 导航骨架与路由表"
**Files:**
- Create: `feature/home/HomeViewModel.kt`
- Create: `AppViewModelProvider.kt`(原属 Task 4,见 ledger Ruling R2)
- Modify: `feature/home/HomeScreen.kt`(完整实现)
- Test: `app/src/test/java/com/placeholder/ringhealth/feature/home/HomeViewModelTest.kt`
- Modify: `res/values/strings.xml`
**Interfaces:**
- Consumes: `HealthDataRepository`、`RingDeviceRepository`、`AppViewModelProvider.Factory`、Task 3 组件
- Consumes: `HealthDataRepository`、`RingDeviceRepository`、`RingHealthApplication.container`、Task 3 组件
- Produces:
- `data class HomeUiState(val loading: Boolean, val summary: HealthSummary?, val device: RingDeviceState?)`
- `class HomeViewModel(healthDataRepository, ringDeviceRepository)` 暴露 `uiState: StateFlow<HomeUiState>`
- `object AppViewModelProvider { val Factory }` 与 `fun CreationExtras.ringHealthApplication(): RingHealthApplication`
- `@Composable fun HomeScreen(onOpenDetail: (MetricType) -> Unit, modifier, viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory))`
- [ ] **Step 1: 写失败的测试**
@@ -2209,27 +2153,43 @@ cd /d/cherry/repos/ring-health-app && ./gradlew :app:testDebugUnitTest --tests "
Expected: `BUILD SUCCESSFUL`。
- [ ] **Step 5: 追加首页文案到 `strings.xml`**
- [ ] **Step 5: 写 `AppViewModelProvider.kt`**
```xml
<string name="home_greeting">今天也要保持活力</string>
<string name="home_device_connected">%1$s · 已连接</string>
<string name="home_device_disconnected">未连接设备</string>
<string name="home_battery">电量 %1$d%%</string>
<string name="home_steps_title">今日步数</string>
<string name="home_steps_goal">目标 %1$s 步</string>
<string name="home_calories_label">千卡</string>
<string name="home_distance_label">公里</string>
<string name="home_metrics">健康指标</string>
<string name="home_not_enabled">未开启</string>
<string name="home_tap_to_enable">点击了解</string>
<string name="unit_bpm">次/分</string>
<string name="unit_percent">%</string>
<string name="unit_celsius">°C</string>
<string name="unit_hour">小时</string>
<string name="unit_minute">分钟</string>
本任务是 `AppViewModelProvider` 的第一个消费者,文件在这里建立(ledger Ruling R2)。
Task 9 / Task 10 会各自往 `viewModelFactory` 里**追加一个 `initializer`**,文件位置与职责不变。
```kotlin
package com.placeholder.ringhealth
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.placeholder.ringhealth.feature.home.HomeViewModel
/**
* ViewModel 工厂:把容器中的仓库注入各 ViewModel。
* 每个 ViewModel 在这里注册一个 initializer;后续任务按需追加。
*/
object AppViewModelProvider {
val Factory = viewModelFactory {
initializer {
HomeViewModel(
healthDataRepository = ringHealthApplication().container.healthDataRepository,
ringDeviceRepository = ringHealthApplication().container.ringDeviceRepository,
)
}
}
}
/** 从 CreationExtras 中取出 Application 实例。 */
fun CreationExtras.ringHealthApplication(): RingHealthApplication =
this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as RingHealthApplication
```
> 说明:`MetricDetailViewModel` 需要运行时参数(指标类型),使用独立的工厂,见 Task 12。
- [ ] **Step 6: 完整实现 `HomeScreen.kt`**
```kotlin
@@ -2513,9 +2473,22 @@ private fun HomeContentPreview() {
> 说明:`buildMetricCards` 是 `@Composable`(内部调用了 `stringResource`),而 `LazyColumn` 的 content lambda 不是 Composable 上下文,所以必须在进入 `LazyColumn` 之前取值——上面的 `val cards = ...` 就是为此。
- [ ] **Step 7: 追加指标名文案到 `strings.xml`**
- [ ] **Step 7: 追加首页文案与指标名到 `strings.xml`**
```xml
<string name="home_device_connected">%1$s · 已连接</string>
<string name="home_device_disconnected">未连接设备</string>
<string name="home_battery">电量 %1$d%%</string>
<string name="home_steps_title">今日步数</string>
<string name="home_steps_goal">目标 %1$s 步</string>
<string name="home_calories_label">千卡</string>
<string name="home_distance_label">公里</string>
<string name="home_metrics">健康指标</string>
<string name="home_not_enabled">未开启</string>
<string name="unit_bpm">次/分</string>
<string name="unit_percent">%</string>
<string name="unit_celsius">°C</string>
<string name="metric_heart_rate">心率</string>
<string name="metric_sleep">睡眠</string>
<string name="metric_blood_oxygen">血氧</string>
@@ -2525,6 +2498,8 @@ private fun HomeContentPreview() {
<string name="metric_step">步数</string>
```
> 这批 `metric_*` 字符串同时供 Task 12 的详情页标题使用(ledger Ruling R3),是它们的唯一来源。
- [ ] **Step 8: 构建 + 单测 + 真机验证**
```bash
@@ -2594,6 +2569,19 @@ class WorkoutViewModel(
}
```
- [ ] **Step 1b: 把 `WorkoutViewModel` 注册到 `AppViewModelProvider`**
在 `AppViewModelProvider.kt` 的 `viewModelFactory { }` 内**追加**一个 `initializer`(ledger Ruling R2),
并在该文件顶部加 `import com.placeholder.ringhealth.feature.workout.WorkoutViewModel`:
```kotlin
initializer {
WorkoutViewModel(
healthDataRepository = ringHealthApplication().container.healthDataRepository,
)
}
```
- [ ] **Step 2: 追加文案到 `strings.xml`**
```xml
@@ -2894,6 +2882,19 @@ class DeviceViewModel(
}
```
- [ ] **Step 1b: 把 `DeviceViewModel` 注册到 `AppViewModelProvider`**
在 `AppViewModelProvider.kt` 的 `viewModelFactory { }` 内**追加**一个 `initializer`(ledger Ruling R2),
并在该文件顶部加 `import com.placeholder.ringhealth.feature.device.DeviceViewModel`:
```kotlin
initializer {
DeviceViewModel(
ringDeviceRepository = ringHealthApplication().container.ringDeviceRepository,
)
}
```
- [ ] **Step 2: 追加文案到 `strings.xml`**
```xml
@@ -3360,12 +3361,17 @@ git add -A && git commit -m "feat: 实现我的页"
- Modify: `res/values/strings.xml`
**Interfaces:**
- Consumes: `HealthDataRepository`、`MetricBarChart`、`StatBar`
- Consumes: `HealthDataRepository`、`MetricBarChart`、`StatBar`、Task 8 加入的 `R.string.metric_*`
- Produces:
- `data class MetricDetailUiState(val loading: Boolean, val metric: MetricType, val range: DateRange, val points: List<MetricPoint>)`
- `class MetricDetailViewModel(metric: MetricType, healthDataRepository)`,方法 `fun switchRange(range: DateRange)`
- `@Composable fun MetricDetailScreen(metric: MetricType, onBack: () -> Unit, modifier, viewModel: MetricDetailViewModel = viewModel(factory = MetricDetailViewModel.factory(metric)))`
- `MetricDetailViewModel.factory(metric: MetricType): ViewModelProvider.Factory`(伴生对象)
- `@Composable private fun metricTitleRes(type: MetricType): Int`(文件内私有)
> **两点范围说明**:
> - 标题用 `metricTitleRes` 映射到 `R.string.metric_*`,**不再有** `metricDisplayName`(ledger Ruling R3)。
> - 增加睡眠专属区块(`StatBar` 分期条 + 入睡/起床时间),补齐 spec §6 的 `detail/sleep` 要求(ledger Ruling R4)。
- [ ] **Step 1: 写 `MetricDetailViewModel.kt`**
@@ -3460,24 +3466,28 @@ class MetricDetailViewModel(
```xml
<string name="detail_range_week">近 7 天</string>
<string name="detail_range_month">近 30 天</string>
<string name="detail_average">平均 %1$s</string>
<string name="detail_max">最高 %1$s</string>
<string name="detail_min">最低 %1$s</string>
<string name="detail_average">平均</string>
<string name="detail_max">最高</string>
<string name="detail_min">最低</string>
<string name="detail_trend">趋势</string>
<string name="detail_sleep_stages">睡眠分期</string>
<string name="detail_sleep_deep">深睡</string>
<string name="detail_sleep_light">浅睡</string>
<string name="detail_sleep_awake">清醒</string>
<string name="detail_not_enabled_title">该指标尚未开启</string>
<string name="detail_not_enabled_desc">前往设备页开启对应的自动监测功能。</string>
<string name="detail_sleep_fall_asleep">入睡时间</string>
<string name="detail_sleep_wake_up">起床时间</string>
<string name="action_back">返回</string>
```
> `detail_average` / `detail_max` / `detail_min` 是**纯标签**,不带 `%1$s`:
> `StatLine` 把标签和数值分成左右两栏渲染,标签里再带占位符会原样打印出 `%1$s`。
- [ ] **Step 3: 实现 `MetricDetailScreen.kt`(替换占位)**
```kotlin
package com.placeholder.ringhealth.feature.detail
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -3486,7 +3496,9 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
@@ -3502,18 +3514,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.placeholder.ringhealth.R
import com.placeholder.ringhealth.core.common.metricDisplayName
import com.placeholder.ringhealth.core.common.formatClock
import com.placeholder.ringhealth.core.designsystem.MetricBlue
import com.placeholder.ringhealth.core.designsystem.MetricGreen
import com.placeholder.ringhealth.core.designsystem.TextSecondary
import com.placeholder.ringhealth.core.designsystem.component.HealthCard
import com.placeholder.ringhealth.core.designsystem.component.MetricBarChart
import com.placeholder.ringhealth.core.designsystem.component.SectionHeader
import com.placeholder.ringhealth.core.designsystem.component.StatBar
import com.placeholder.ringhealth.core.designsystem.component.StatSegment
import com.placeholder.ringhealth.domain.model.DateRange
import com.placeholder.ringhealth.domain.model.MetricPoint
import com.placeholder.ringhealth.domain.model.MetricType
/**
@@ -3534,7 +3552,7 @@ fun MetricDetailScreen(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
TopAppBar(
title = { Text(text = metricDisplayName(metric)) },
title = { Text(text = stringResource(metricTitleRes(metric))) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
@@ -3579,10 +3597,88 @@ fun MetricDetailScreen(
StatLine(label = stringResource(R.string.detail_min), value = values.minOrNull() ?: 0.0)
}
}
if (state.metric == MetricType.SLEEP) {
item { SleepStagesCard(points = state.points) }
}
}
}
}
/**
* 睡眠分期卡片:深睡/浅睡/清醒分段条 + 入睡/起床时间(对应 spec §6 的 `detail/sleep`)。
*/
@Composable
private fun SleepStagesCard(points: List<MetricPoint>, modifier: Modifier = Modifier) {
// 阶段一没有真实分期数据,按固定比例演示;M3 接入真实睡眠数据后改为按实际时段计算。
val segments = listOf(
StatSegment(weight = 0.25f, color = MetricBlue),
StatSegment(weight = 0.65f, color = MetricBlue.copy(alpha = 0.45f)),
StatSegment(weight = 0.10f, color = MetricBlue.copy(alpha = 0.2f)),
)
HealthCard(modifier = modifier) {
SectionHeader(title = stringResource(R.string.detail_sleep_stages))
StatBar(segments = segments)
Row(
modifier = Modifier.fillMaxWidth().padding(top = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
StageLegend(label = stringResource(R.string.detail_sleep_deep), tint = MetricBlue)
StageLegend(label = stringResource(R.string.detail_sleep_light), tint = MetricBlue.copy(alpha = 0.45f))
StageLegend(label = stringResource(R.string.detail_sleep_awake), tint = MetricBlue.copy(alpha = 0.2f))
}
val fallAsleep = points.firstOrNull()?.timestamp
val wakeUp = points.lastOrNull()?.timestamp
if (fallAsleep != null && wakeUp != null) {
Row(
modifier = Modifier.fillMaxWidth().padding(top = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = "${stringResource(R.string.detail_sleep_fall_asleep)} ${formatClock(fallAsleep)}",
style = MaterialTheme.typography.bodySmall,
color = TextSecondary,
)
Text(
text = "${stringResource(R.string.detail_sleep_wake_up)} ${formatClock(wakeUp)}",
style = MaterialTheme.typography.bodySmall,
color = TextSecondary,
)
}
}
}
}
/** 分期图例:小色块 + 名称。 */
@Composable
private fun StageLegend(label: String, tint: Color, modifier: Modifier = Modifier) {
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(10.dp)
.clip(RoundedCornerShape(5.dp))
.background(tint),
)
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = TextSecondary,
modifier = Modifier.padding(start = 6.dp),
)
}
}
/** 指标类型 → 标题字符串资源。文案的唯一来源是 `strings.xml`(ledger Ruling R3)。 */
@Composable
private fun metricTitleRes(type: MetricType): Int = when (type) {
MetricType.STEP -> R.string.metric_step
MetricType.HEART_RATE -> R.string.metric_heart_rate
MetricType.SLEEP -> R.string.metric_sleep
MetricType.BLOOD_OXYGEN -> R.string.metric_blood_oxygen
MetricType.STRESS -> R.string.metric_stress
MetricType.TEMPERATURE -> R.string.metric_temperature
MetricType.BLOOD_PRESSURE -> R.string.metric_blood_pressure
}
/** 周/月切换。 */
@Composable
private fun RangeSelector(
@@ -3646,7 +3742,7 @@ private fun List<Double>.averageOrZero(): Double = if (isEmpty()) 0.0 else avera
cd /d/cherry/repos/ring-health-app && ./gradlew assembleDebug && ./gradlew installDebug
```
Expected: 从首页点任一指标卡进入详情页;顶部标题为指标中文名;近 7 天/近 30 天可切换,柱状图数量随之变化(7 根 / 30 根);返回键回到首页。
Expected: 从首页点任一指标卡进入详情页;顶部标题为指标中文名;近 7 天/近 30 天可切换,柱状图数量随之变化(7 根 / 30 根);**睡眠详情额外显示分期卡片**(深睡/浅睡/清醒分段条与图例、入睡/起床时间),其他指标不显示;返回键回到首页。
- [ ] **Step 6: Commit**
@@ -3832,6 +3928,7 @@ cd /d/cherry/repos/ring-health-app && ./gradlew installDebug
- [ ] 首页:六个指标卡正常显示,**血压显示“未开启”**
- [ ] 首页:点击任一指标卡进入详情页,标题为该指标中文名
- [ ] 详情页:近 7 天显示 7 根柱子,切到近 30 天显示 30 根柱子
- [ ] 睡眠详情额外显示分期卡片(深睡/浅睡/清醒分段条 + 入睡/起床时间),其他指标没有这张卡
- [ ] 详情页:返回后回到首页
- [ ] 运动页:显示“开始运动”卡片 + 5 条运动记录
- [ ] 运动页:“查看全部”进入历史页,底部导航栏仍可见