From 6d3cb1b4184d5f72cfc25253153dddfb0db5b557 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:54:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=BA=95=E9=83=A8?= =?UTF-8?q?=E5=9B=9B=20Tab=20=E5=AF=BC=E8=88=AA=E9=AA=A8=E6=9E=B6=E4=B8=8E?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeholder/ringhealth/MainActivity.kt | 11 +- .../core/navigation/RingHealthApp.kt | 117 ++++++++++++++++++ .../ringhealth/core/navigation/Routes.kt | 23 ++++ .../feature/detail/MetricDetailScreen.kt | 19 +++ .../ringhealth/feature/device/DeviceScreen.kt | 18 +++ .../feature/device/DeviceSettingsScreen.kt | 18 +++ .../ringhealth/feature/home/HomeScreen.kt | 22 ++++ .../feature/profile/ProfileScreen.kt | 18 +++ .../feature/workout/WorkoutHistoryScreen.kt | 18 +++ .../feature/workout/WorkoutScreen.kt | 18 +++ app/src/main/res/values/strings.xml | 8 ++ 11 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/placeholder/ringhealth/core/navigation/RingHealthApp.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/core/navigation/Routes.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceScreen.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceSettingsScreen.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/feature/home/HomeScreen.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/feature/profile/ProfileScreen.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutHistoryScreen.kt create mode 100644 app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutScreen.kt diff --git a/app/src/main/java/com/placeholder/ringhealth/MainActivity.kt b/app/src/main/java/com/placeholder/ringhealth/MainActivity.kt index 724daf1..f6fef30 100644 --- a/app/src/main/java/com/placeholder/ringhealth/MainActivity.kt +++ b/app/src/main/java/com/placeholder/ringhealth/MainActivity.kt @@ -4,15 +4,12 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import com.placeholder.ringhealth.core.designsystem.RingHealthTheme +import com.placeholder.ringhealth.core.navigation.RingHealthApp /** 唯一 Activity,承载 Compose 界面。 */ class MainActivity : ComponentActivity() { @@ -25,10 +22,8 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - // 文案取自 res/values/strings.xml,不在 Kotlin 里硬编码用户可见文字 - Text(stringResource(R.string.app_name)) - } + // 界面骨架(底部导航 + 路由)在 core/navigation/RingHealthApp.kt + RingHealthApp() } } } diff --git a/app/src/main/java/com/placeholder/ringhealth/core/navigation/RingHealthApp.kt b/app/src/main/java/com/placeholder/ringhealth/core/navigation/RingHealthApp.kt new file mode 100644 index 0000000..5bf619b --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/core/navigation/RingHealthApp.kt @@ -0,0 +1,117 @@ +package com.placeholder.ringhealth.core.navigation + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FitnessCenter +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.placeholder.ringhealth.R +import com.placeholder.ringhealth.domain.model.MetricType +import com.placeholder.ringhealth.feature.device.DeviceScreen +import com.placeholder.ringhealth.feature.device.DeviceSettingsPlaceholder +import com.placeholder.ringhealth.feature.detail.MetricDetailPlaceholder +import com.placeholder.ringhealth.feature.home.HomeScreen +import com.placeholder.ringhealth.feature.profile.ProfileScreen +import com.placeholder.ringhealth.feature.workout.WorkoutHistoryPlaceholder +import com.placeholder.ringhealth.feature.workout.WorkoutScreen + +/** 底部导航项定义。 */ +private data class BottomTab( + val route: String, + val labelRes: Int, + val icon: ImageVector, +) + +private val BOTTOM_TABS = listOf( + BottomTab(Routes.HOME, R.string.tab_home, Icons.Filled.Home), + BottomTab(Routes.WORKOUT, R.string.tab_workout, Icons.Filled.FitnessCenter), + BottomTab(Routes.DEVICE, R.string.tab_device, Icons.Filled.Settings), + BottomTab(Routes.PROFILE, R.string.tab_profile, Icons.Filled.Person), +) + +/** + * 应用主界面:Scaffold + 底部导航 + NavHost。 + */ +@Composable +fun RingHealthApp(navController: NavHostController = rememberNavController()) { + val backStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = backStackEntry?.destination?.route + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + bottomBar = { + NavigationBar(containerColor = MaterialTheme.colorScheme.surface) { + BOTTOM_TABS.forEach { tab -> + NavigationBarItem( + selected = currentRoute == tab.route, + onClick = { navController.navigateToTab(tab.route) }, + icon = { Icon(imageVector = tab.icon, contentDescription = null) }, + label = { Text(stringResource(tab.labelRes)) }, + ) + } + } + }, + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = Routes.HOME, + modifier = Modifier.padding(innerPadding), + ) { + composable(Routes.HOME) { + HomeScreen(onOpenDetail = { metric -> navController.navigate(Routes.detail(metric)) }) + } + composable(Routes.WORKOUT) { + WorkoutScreen() + } + composable(Routes.DEVICE) { + DeviceScreen() + } + composable(Routes.PROFILE) { + ProfileScreen() + } + composable(Routes.DEVICE_SETTINGS) { + DeviceSettingsPlaceholder() + } + composable(Routes.WORKOUT_HISTORY) { + WorkoutHistoryPlaceholder() + } + composable( + route = Routes.DETAIL_PATTERN, + arguments = listOf(navArgument(Routes.DETAIL_ARG_METRIC) { type = NavType.StringType }), + ) { entry -> + val raw = entry.arguments?.getString(Routes.DETAIL_ARG_METRIC).orEmpty() + val metric = MetricType.entries.firstOrNull { it.name == raw } ?: MetricType.STEP + MetricDetailPlaceholder(metric = metric) + } + } + } +} + +/** 底部 Tab 切换:保留状态、避免回退栈堆积。 */ +private fun NavHostController.navigateToTab(route: String) { + navigate(route) { + popUpTo(graph.findStartDestination().id) { saveState = true } + launchSingleTop = true + restoreState = true + } +} diff --git a/app/src/main/java/com/placeholder/ringhealth/core/navigation/Routes.kt b/app/src/main/java/com/placeholder/ringhealth/core/navigation/Routes.kt new file mode 100644 index 0000000..87bac9c --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/core/navigation/Routes.kt @@ -0,0 +1,23 @@ +package com.placeholder.ringhealth.core.navigation + +import com.placeholder.ringhealth.domain.model.MetricType + +/** + * 全局路由表。字符串集中定义,避免散落在各处。 + */ +object Routes { + const val HOME = "home" + const val WORKOUT = "workout" + const val DEVICE = "device" + const val PROFILE = "profile" + + const val DEVICE_SETTINGS = "device/settings" + const val WORKOUT_HISTORY = "workout/history" + + /** 指标详情路由模板,参数为 MetricType.name。 */ + const val DETAIL_PATTERN = "detail/{metric}" + const val DETAIL_ARG_METRIC = "metric" + + /** 拼出某个指标的详情路由。 */ + fun detail(metric: MetricType): String = "detail/${metric.name}" +} diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt new file mode 100644 index 0000000..62e2e6d --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/feature/detail/MetricDetailScreen.kt @@ -0,0 +1,19 @@ +package com.placeholder.ringhealth.feature.detail + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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 实现)。 */ +@Composable +fun MetricDetailPlaceholder(metric: MetricType, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(stringResource(R.string.placeholder_metric_detail)) + } +} diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceScreen.kt new file mode 100644 index 0000000..31f494f --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceScreen.kt @@ -0,0 +1,18 @@ +package com.placeholder.ringhealth.feature.device + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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(stringResource(R.string.tab_device)) + } +} diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceSettingsScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceSettingsScreen.kt new file mode 100644 index 0000000..813ec9e --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/feature/device/DeviceSettingsScreen.kt @@ -0,0 +1,18 @@ +package com.placeholder.ringhealth.feature.device + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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 DeviceSettingsPlaceholder(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(stringResource(R.string.placeholder_device_settings)) + } +} diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/home/HomeScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/home/HomeScreen.kt new file mode 100644 index 0000000..249bc6f --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/feature/home/HomeScreen.kt @@ -0,0 +1,22 @@ +package com.placeholder.ringhealth.feature.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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(stringResource(R.string.tab_home)) + } +} diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/profile/ProfileScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/profile/ProfileScreen.kt new file mode 100644 index 0000000..1848493 --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/feature/profile/ProfileScreen.kt @@ -0,0 +1,18 @@ +package com.placeholder.ringhealth.feature.profile + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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(stringResource(R.string.tab_profile)) + } +} diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutHistoryScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutHistoryScreen.kt new file mode 100644 index 0000000..eb31962 --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutHistoryScreen.kt @@ -0,0 +1,18 @@ +package com.placeholder.ringhealth.feature.workout + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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 WorkoutHistoryPlaceholder(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(stringResource(R.string.placeholder_workout_history)) + } +} diff --git a/app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutScreen.kt b/app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutScreen.kt new file mode 100644 index 0000000..2d20585 --- /dev/null +++ b/app/src/main/java/com/placeholder/ringhealth/feature/workout/WorkoutScreen.kt @@ -0,0 +1,18 @@ +package com.placeholder.ringhealth.feature.workout + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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(stringResource(R.string.tab_workout)) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa4e8ff..001209e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,4 +6,12 @@ 正常 中等 偏高 + + 健康 + 运动 + 设备 + 我的 + 设备设置 + 运动历史 + 指标详情