From 42bc9fe12e1d2441689a3cf7a0a6deb339d1e883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=94=E9=B2=81=E6=AD=A3?= <13780741225@163.com> Date: Thu, 10 Sep 2026 10:32:55 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=E5=B1=82=E4=B8=8E=E5=8D=A0=E4=BD=8D=E9=A1=B5=E7=9A=84?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81=E6=96=87=E6=A1=88=EF=BC=88ledger=20?= =?UTF-8?q?R13/R14=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - formatCalories 不再拼接「千卡」(与 R.string.home_calories_label 重复), workout_detail_calories 改为「消耗 %1$s 千卡」 - stressLevelLabel(String) 改为 @StringRes stressLevelLabelRes(Int),描述进 strings.xml - Global Constraints 写明唯一豁免(formatSleepDuration/formatDuration 的复合时长值)及判据 - Task 7 的 7 个占位 Composable 改用 stringResource,补齐 import 与 3 条占位文案 --- .../plans/2026-09-10-ring-health-app-m0-m1.md | 110 +++++++++++++----- 1 file changed, 78 insertions(+), 32 deletions(-) diff --git a/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md b/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md index 6edee98..439d5a0 100644 --- a/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md +++ b/superpowers/plans/2026-09-10-ring-health-app-m0-m1.md @@ -18,6 +18,13 @@ - **工具链钉死**:Gradle `8.14`、AGP `8.13.1`、Kotlin `2.2.20`(三者均为本机缓存版本)。 - **依赖约束**:阶段一**不引入** Hilt、Room、Retrofit、第三方图表库。图表一律用 Compose `Canvas` 手绘。 - **不硬编码文案**:所有用户可见文字进 `res/values/strings.xml`(默认中文)。 + **唯一豁免**(见 ledger Ruling R13):`core/common/Formatters.kt` 中 + `formatSleepDuration`(`7小时32分`)与 `formatDuration`(`1小时05分`)这两条 —— + 它们是数字与单位**黏合成单一复合值**、且设计系统没有独立单位槽位可放的时长排版, + 拆开会把同一段 `when` 逻辑复制进每个调用点。除此之外,格式化函数**一律不得**拼接 + 用户可见文字:单位与描述词要么由调用点用资源提供(如 `formatCalories` 的「千卡」), + 要么以 `@StringRes` 返回资源 id(如 `stressLevelLabelRes`)。判据是 + 「该文字是否已在 `strings.xml` 里有第二份」与「设计系统是否已提供独立槽位」。 - **禁止行内样式散落**:颜色/字号统一定义在 `core/designsystem/`,页面不得直接写 `Color(0xFF...)`。 - **命名规范**:类/Composable 用 PascalCase;函数/变量用 camelCase;资源文件用 kebab-case/snake_case。 `const val` 编译期常量用 UPPER_SNAKE_CASE。 @@ -1165,12 +1172,20 @@ git add -A && git commit -m "feat: 新增领域模型与仓库接口" - `formatDuration(minutes: Int): String` - `formatClock(timestamp: Long): String` - `formatDate(timestamp: Long): String` - - `stressLevelLabel(stress: Int): String` + - `stressLevelLabelRes(stress: Int): Int`(带 `@StringRes` 标注) > **范围说明(见 ledger Ruling R3)**:本任务**不提供** `metricDisplayName(type)`。 > 指标中文名属于用户可见文案,Global Constraints 要求一律进 `strings.xml`; > 把它硬编码在 Kotlin 里会与 Task 8 加入 `R.string.metric_*` 形成两份事实来源。 > 详情页标题改用资源 id 映射,见 Task 12。 +> +> **范围说明(见 ledger Ruling R13)**:本任务**不得**在 Kotlin 里拼接用户可见文字,两处按此调整: +> ①`formatCalories` 只返回千分位数字,「千卡」由调用点用资源拼接 —— 硬编码会与 +> `R.string.home_calories_label` 形成两份事实来源,正是 R3 排除 `metricDisplayName` 的同一条理由; +> ②压力等级**改为返回字符串资源 id**(`stressLevelLabelRes`),中文描述进 `strings.xml`, +> 与 R3 对指标名的处理保持一致。**唯一豁免**见 Global Constraints 的说明 +> (`formatSleepDuration` / `formatDuration` 的「7小时32分」这类数字+单位黏合复合值)。 +> 本任务同时负责新增这几条字符串资源(见 Step 3)。 - [ ] **Step 1: 写失败的测试** @@ -1179,6 +1194,7 @@ git add -A && git commit -m "feat: 新增领域模型与仓库接口" ```kotlin package com.placeholder.ringhealth.core.common +import com.placeholder.ringhealth.R import org.junit.Assert.assertEquals import org.junit.Test @@ -1212,16 +1228,17 @@ class FormattersTest { } @Test - fun `formatCalories 添加单位`() { - assertEquals("327 千卡", formatCalories(327)) + fun `formatCalories 只返回千分位数字`() { + assertEquals("327", formatCalories(327)) + assertEquals("1,234", formatCalories(1234)) } @Test - fun `stressLevelLabel 按区间返回中文描述`() { - assertEquals("放松", stressLevelLabel(20)) - assertEquals("正常", stressLevelLabel(45)) - assertEquals("中等", stressLevelLabel(65)) - assertEquals("偏高", stressLevelLabel(90)) + fun `stressLevelLabelRes 按区间映射到对应字符串资源`() { + assertEquals(R.string.stress_relaxed, stressLevelLabelRes(20)) + assertEquals(R.string.stress_normal, stressLevelLabelRes(45)) + assertEquals(R.string.stress_medium, stressLevelLabelRes(65)) + assertEquals(R.string.stress_high, stressLevelLabelRes(90)) } } ``` @@ -1241,6 +1258,8 @@ Expected: 编译失败或测试失败(`formatSleepDuration` 等未定义 / `Un ```kotlin package com.placeholder.ringhealth.core.common +import androidx.annotation.StringRes +import com.placeholder.ringhealth.R import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -1251,8 +1270,8 @@ fun formatSteps(steps: Int): String = String.format(Locale.CHINA, "%,d", steps) /** 距离:保留两位小数(单位 km 由 UI 单独展示)。 */ fun formatDistance(km: Double): String = String.format(Locale.CHINA, "%.2f", km) -/** 卡路里:带中文单位。 */ -fun formatCalories(kcal: Int): String = "$kcal 千卡" +/** 卡路里:只做千分位,单位「千卡」由 UI 用字符串资源拼接(ledger Ruling R13)。 */ +fun formatCalories(kcal: Int): String = String.format(Locale.CHINA, "%,d", kcal) /** 睡眠时长:7小时32分 / 45分 / 8小时。 */ fun formatSleepDuration(minutes: Int): String { @@ -1284,15 +1303,25 @@ fun formatClock(timestamp: Long): String = fun formatDate(timestamp: Long): String = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA).format(Date(timestamp)) -/** 压力值 → 中文等级。 */ -fun stressLevelLabel(stress: Int): String = when { - stress < 30 -> "放松" - stress < 60 -> "正常" - stress < 80 -> "中等" - else -> "偏高" +/** 压力值 → 字符串资源 id。中文描述的唯一来源是 `strings.xml`(ledger Ruling R13)。 */ +@StringRes +fun stressLevelLabelRes(stress: Int): Int = when { + stress < 30 -> R.string.stress_relaxed + stress < 60 -> R.string.stress_normal + stress < 80 -> R.string.stress_medium + else -> R.string.stress_high } ``` +同时把本任务新增的文案追加进 `app/src/main/res/values/strings.xml` 的 `` 内: + +```xml + 放松 + 正常 + 中等 + 偏高 +``` + - [ ] **Step 4: 运行测试,确认通过** ```bash @@ -1720,16 +1749,18 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.placeholder.ringhealth.R import com.placeholder.ringhealth.domain.model.MetricType -/** 健康首页。 */ +/** 健康首页(占位,待 Task 8 实现)。 */ @Composable fun HomeScreen( onOpenDetail: (MetricType) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("健康首页") + Text(stringResource(R.string.tab_home)) } } ``` @@ -1745,12 +1776,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.placeholder.ringhealth.R -/** 运动页。 */ +/** 运动页(占位,待 Task 9 实现)。 */ @Composable fun WorkoutScreen(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("运动") + Text(stringResource(R.string.tab_workout)) } } ``` @@ -1766,12 +1799,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.placeholder.ringhealth.R -/** 设备页。 */ +/** 设备页(占位,待 Task 10 实现)。 */ @Composable fun DeviceScreen(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("设备") + Text(stringResource(R.string.tab_device)) } } ``` @@ -1787,12 +1822,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.placeholder.ringhealth.R -/** 我的页。 */ +/** 我的页(占位,待 Task 11 实现)。 */ @Composable fun ProfileScreen(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("我的") + Text(stringResource(R.string.tab_profile)) } } ``` @@ -1806,6 +1843,9 @@ fun ProfileScreen(modifier: Modifier = Modifier) { 运动 设备 我的 + 设备设置 + 运动历史 + 指标详情 ``` - [ ] **Step 4: 写 `RingHealthApp.kt`** @@ -1943,12 +1983,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.placeholder.ringhealth.R -/** 设备设置子页(待 Task 10 实现)。 */ +/** 设备设置子页(占位,待 Task 10 实现)。 */ @Composable fun DeviceSettingsPlaceholder(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("设备设置") + Text(stringResource(R.string.placeholder_device_settings)) } } ``` @@ -1964,12 +2006,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.placeholder.ringhealth.R -/** 运动历史子页(待 Task 9 实现)。 */ +/** 运动历史子页(占位,待 Task 9 实现)。 */ @Composable fun WorkoutHistoryPlaceholder(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("运动历史") + Text(stringResource(R.string.placeholder_workout_history)) } } ``` @@ -1985,13 +2029,15 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.placeholder.ringhealth.R import com.placeholder.ringhealth.domain.model.MetricType -/** 指标详情子页(待 Task 12 实现)。 */ +/** 指标详情子页(占位,待 Task 12 实现)。 */ @Composable fun MetricDetailPlaceholder(metric: MetricType, modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("详情:${metric.name}") + Text(stringResource(R.string.placeholder_metric_detail)) } } ``` @@ -2394,7 +2440,7 @@ private fun StepsCard(summary: HealthSummary, onClick: () -> Unit, modifier: Mod tint = MetricBlue, ) SummaryMetric( - value = summary.calories.toString(), + value = formatCalories(summary.calories), label = stringResource(R.string.home_calories_label), tint = MetricOrange, ) @@ -2602,7 +2648,7 @@ class WorkoutViewModel( 暂无运动记录 %1$s · %2$s 公里 时长 %1$s - 消耗 %1$s + 消耗 %1$s 千卡 查看全部 ```