docs: 修正格式化层与占位页的硬编码文案(ledger R13/R14)
- 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 条占位文案
This commit is contained in:
@@ -18,6 +18,13 @@
|
|||||||
- **工具链钉死**:Gradle `8.14`、AGP `8.13.1`、Kotlin `2.2.20`(三者均为本机缓存版本)。
|
- **工具链钉死**:Gradle `8.14`、AGP `8.13.1`、Kotlin `2.2.20`(三者均为本机缓存版本)。
|
||||||
- **依赖约束**:阶段一**不引入** Hilt、Room、Retrofit、第三方图表库。图表一律用 Compose `Canvas` 手绘。
|
- **依赖约束**:阶段一**不引入** Hilt、Room、Retrofit、第三方图表库。图表一律用 Compose `Canvas` 手绘。
|
||||||
- **不硬编码文案**:所有用户可见文字进 `res/values/strings.xml`(默认中文)。
|
- **不硬编码文案**:所有用户可见文字进 `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...)`。
|
- **禁止行内样式散落**:颜色/字号统一定义在 `core/designsystem/`,页面不得直接写 `Color(0xFF...)`。
|
||||||
- **命名规范**:类/Composable 用 PascalCase;函数/变量用 camelCase;资源文件用 kebab-case/snake_case。
|
- **命名规范**:类/Composable 用 PascalCase;函数/变量用 camelCase;资源文件用 kebab-case/snake_case。
|
||||||
`const val` 编译期常量用 UPPER_SNAKE_CASE。
|
`const val` 编译期常量用 UPPER_SNAKE_CASE。
|
||||||
@@ -1165,12 +1172,20 @@ git add -A && git commit -m "feat: 新增领域模型与仓库接口"
|
|||||||
- `formatDuration(minutes: Int): String`
|
- `formatDuration(minutes: Int): String`
|
||||||
- `formatClock(timestamp: Long): String`
|
- `formatClock(timestamp: Long): String`
|
||||||
- `formatDate(timestamp: Long): String`
|
- `formatDate(timestamp: Long): String`
|
||||||
- `stressLevelLabel(stress: Int): String`
|
- `stressLevelLabelRes(stress: Int): Int`(带 `@StringRes` 标注)
|
||||||
|
|
||||||
> **范围说明(见 ledger Ruling R3)**:本任务**不提供** `metricDisplayName(type)`。
|
> **范围说明(见 ledger Ruling R3)**:本任务**不提供** `metricDisplayName(type)`。
|
||||||
> 指标中文名属于用户可见文案,Global Constraints 要求一律进 `strings.xml`;
|
> 指标中文名属于用户可见文案,Global Constraints 要求一律进 `strings.xml`;
|
||||||
> 把它硬编码在 Kotlin 里会与 Task 8 加入 `R.string.metric_*` 形成两份事实来源。
|
> 把它硬编码在 Kotlin 里会与 Task 8 加入 `R.string.metric_*` 形成两份事实来源。
|
||||||
> 详情页标题改用资源 id 映射,见 Task 12。
|
> 详情页标题改用资源 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: 写失败的测试**
|
- [ ] **Step 1: 写失败的测试**
|
||||||
|
|
||||||
@@ -1179,6 +1194,7 @@ git add -A && git commit -m "feat: 新增领域模型与仓库接口"
|
|||||||
```kotlin
|
```kotlin
|
||||||
package com.placeholder.ringhealth.core.common
|
package com.placeholder.ringhealth.core.common
|
||||||
|
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -1212,16 +1228,17 @@ class FormattersTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `formatCalories 添加单位`() {
|
fun `formatCalories 只返回千分位数字`() {
|
||||||
assertEquals("327 千卡", formatCalories(327))
|
assertEquals("327", formatCalories(327))
|
||||||
|
assertEquals("1,234", formatCalories(1234))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `stressLevelLabel 按区间返回中文描述`() {
|
fun `stressLevelLabelRes 按区间映射到对应字符串资源`() {
|
||||||
assertEquals("放松", stressLevelLabel(20))
|
assertEquals(R.string.stress_relaxed, stressLevelLabelRes(20))
|
||||||
assertEquals("正常", stressLevelLabel(45))
|
assertEquals(R.string.stress_normal, stressLevelLabelRes(45))
|
||||||
assertEquals("中等", stressLevelLabel(65))
|
assertEquals(R.string.stress_medium, stressLevelLabelRes(65))
|
||||||
assertEquals("偏高", stressLevelLabel(90))
|
assertEquals(R.string.stress_high, stressLevelLabelRes(90))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1241,6 +1258,8 @@ Expected: 编译失败或测试失败(`formatSleepDuration` 等未定义 / `Un
|
|||||||
```kotlin
|
```kotlin
|
||||||
package com.placeholder.ringhealth.core.common
|
package com.placeholder.ringhealth.core.common
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -1251,8 +1270,8 @@ fun formatSteps(steps: Int): String = String.format(Locale.CHINA, "%,d", steps)
|
|||||||
/** 距离:保留两位小数(单位 km 由 UI 单独展示)。 */
|
/** 距离:保留两位小数(单位 km 由 UI 单独展示)。 */
|
||||||
fun formatDistance(km: Double): String = String.format(Locale.CHINA, "%.2f", km)
|
fun formatDistance(km: Double): String = String.format(Locale.CHINA, "%.2f", km)
|
||||||
|
|
||||||
/** 卡路里:带中文单位。 */
|
/** 卡路里:只做千分位,单位「千卡」由 UI 用字符串资源拼接(ledger Ruling R13)。 */
|
||||||
fun formatCalories(kcal: Int): String = "$kcal 千卡"
|
fun formatCalories(kcal: Int): String = String.format(Locale.CHINA, "%,d", kcal)
|
||||||
|
|
||||||
/** 睡眠时长:7小时32分 / 45分 / 8小时。 */
|
/** 睡眠时长:7小时32分 / 45分 / 8小时。 */
|
||||||
fun formatSleepDuration(minutes: Int): String {
|
fun formatSleepDuration(minutes: Int): String {
|
||||||
@@ -1284,15 +1303,25 @@ fun formatClock(timestamp: Long): String =
|
|||||||
fun formatDate(timestamp: Long): String =
|
fun formatDate(timestamp: Long): String =
|
||||||
SimpleDateFormat("yyyy-MM-dd", Locale.CHINA).format(Date(timestamp))
|
SimpleDateFormat("yyyy-MM-dd", Locale.CHINA).format(Date(timestamp))
|
||||||
|
|
||||||
/** 压力值 → 中文等级。 */
|
/** 压力值 → 字符串资源 id。中文描述的唯一来源是 `strings.xml`(ledger Ruling R13)。 */
|
||||||
fun stressLevelLabel(stress: Int): String = when {
|
@StringRes
|
||||||
stress < 30 -> "放松"
|
fun stressLevelLabelRes(stress: Int): Int = when {
|
||||||
stress < 60 -> "正常"
|
stress < 30 -> R.string.stress_relaxed
|
||||||
stress < 80 -> "中等"
|
stress < 60 -> R.string.stress_normal
|
||||||
else -> "偏高"
|
stress < 80 -> R.string.stress_medium
|
||||||
|
else -> R.string.stress_high
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
同时把本任务新增的文案追加进 `app/src/main/res/values/strings.xml` 的 `<resources>` 内:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<string name="stress_relaxed">放松</string>
|
||||||
|
<string name="stress_normal">正常</string>
|
||||||
|
<string name="stress_medium">中等</string>
|
||||||
|
<string name="stress_high">偏高</string>
|
||||||
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: 运行测试,确认通过**
|
- [ ] **Step 4: 运行测试,确认通过**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1720,16 +1749,18 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
import com.placeholder.ringhealth.domain.model.MetricType
|
import com.placeholder.ringhealth.domain.model.MetricType
|
||||||
|
|
||||||
/** 健康首页。 */
|
/** 健康首页(占位,待 Task 8 实现)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
onOpenDetail: (MetricType) -> Unit,
|
onOpenDetail: (MetricType) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
|
|
||||||
/** 运动页。 */
|
/** 运动页(占位,待 Task 9 实现)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun WorkoutScreen(modifier: Modifier = Modifier) {
|
fun WorkoutScreen(modifier: Modifier = Modifier) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
|
|
||||||
/** 设备页。 */
|
/** 设备页(占位,待 Task 10 实现)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceScreen(modifier: Modifier = Modifier) {
|
fun DeviceScreen(modifier: Modifier = Modifier) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
|
|
||||||
/** 我的页。 */
|
/** 我的页(占位,待 Task 11 实现)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(modifier: Modifier = Modifier) {
|
fun ProfileScreen(modifier: Modifier = Modifier) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text("我的")
|
Text(stringResource(R.string.tab_profile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1806,6 +1843,9 @@ fun ProfileScreen(modifier: Modifier = Modifier) {
|
|||||||
<string name="tab_workout">运动</string>
|
<string name="tab_workout">运动</string>
|
||||||
<string name="tab_device">设备</string>
|
<string name="tab_device">设备</string>
|
||||||
<string name="tab_profile">我的</string>
|
<string name="tab_profile">我的</string>
|
||||||
|
<string name="placeholder_device_settings">设备设置</string>
|
||||||
|
<string name="placeholder_workout_history">运动历史</string>
|
||||||
|
<string name="placeholder_metric_detail">指标详情</string>
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: 写 `RingHealthApp.kt`**
|
- [ ] **Step 4: 写 `RingHealthApp.kt`**
|
||||||
@@ -1943,12 +1983,14 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
|
|
||||||
/** 设备设置子页(待 Task 10 实现)。 */
|
/** 设备设置子页(占位,待 Task 10 实现)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceSettingsPlaceholder(modifier: Modifier = Modifier) {
|
fun DeviceSettingsPlaceholder(modifier: Modifier = Modifier) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
|
|
||||||
/** 运动历史子页(待 Task 9 实现)。 */
|
/** 运动历史子页(占位,待 Task 9 实现)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun WorkoutHistoryPlaceholder(modifier: Modifier = Modifier) {
|
fun WorkoutHistoryPlaceholder(modifier: Modifier = Modifier) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.placeholder.ringhealth.R
|
||||||
import com.placeholder.ringhealth.domain.model.MetricType
|
import com.placeholder.ringhealth.domain.model.MetricType
|
||||||
|
|
||||||
/** 指标详情子页(待 Task 12 实现)。 */
|
/** 指标详情子页(占位,待 Task 12 实现)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun MetricDetailPlaceholder(metric: MetricType, modifier: Modifier = Modifier) {
|
fun MetricDetailPlaceholder(metric: MetricType, modifier: Modifier = Modifier) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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,
|
tint = MetricBlue,
|
||||||
)
|
)
|
||||||
SummaryMetric(
|
SummaryMetric(
|
||||||
value = summary.calories.toString(),
|
value = formatCalories(summary.calories),
|
||||||
label = stringResource(R.string.home_calories_label),
|
label = stringResource(R.string.home_calories_label),
|
||||||
tint = MetricOrange,
|
tint = MetricOrange,
|
||||||
)
|
)
|
||||||
@@ -2602,7 +2648,7 @@ class WorkoutViewModel(
|
|||||||
<string name="workout_empty">暂无运动记录</string>
|
<string name="workout_empty">暂无运动记录</string>
|
||||||
<string name="workout_item_summary">%1$s · %2$s 公里</string>
|
<string name="workout_item_summary">%1$s · %2$s 公里</string>
|
||||||
<string name="workout_detail_duration">时长 %1$s</string>
|
<string name="workout_detail_duration">时长 %1$s</string>
|
||||||
<string name="workout_detail_calories">消耗 %1$s</string>
|
<string name="workout_detail_calories">消耗 %1$s 千卡</string>
|
||||||
<string name="action_view_all">查看全部</string>
|
<string name="action_view_all">查看全部</string>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user