package com.rainy.token.ui import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel 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.rainy.token.R import com.rainy.token.data.repository.CommandCodeUsageRepository import com.rainy.token.domain.service.ServiceConfigProvider import com.rainy.token.domain.service.ServiceType import com.rainy.token.ui.components.rememberWindowSizeClass import com.rainy.token.ui.components.DebugLogScreen import com.rainy.token.ui.components.TipsScreen import com.rainy.token.ui.dashboard.DashboardScreen import com.rainy.token.ui.dashboard.DashboardViewModel import com.rainy.token.ui.dashboard.UsageChartViewModel import com.rainy.token.ui.dashboard.UsageDataScreen import com.rainy.token.ui.dashboard.UsageDataViewModel import com.rainy.token.ui.dashboard.UsageDetailScreen import com.rainy.token.ui.dashboard.UsageOverviewScreen import com.rainy.token.ui.dashboard.UsageViewModel import com.rainy.token.ui.heatmap.HeatmapScreen import com.rainy.token.ui.account.ApiManagementScreen import com.rainy.token.ui.servicedetail.ServiceDetailScreen import com.rainy.token.ui.settings.AccountManagementScreen import com.rainy.token.ui.settings.CredentialEditScreen import com.rainy.token.ui.settings.CredentialEditViewModel import com.rainy.token.ui.settings.SettingsScreen import com.rainy.token.ui.theme.inkMuted import com.rainy.token.ui.theme.StrawberryPink import com.rainy.token.ui.webview.CodexOAuthScreen import com.rainy.token.ui.webview.WebViewLoginScreen import com.rainy.token.ui.webview.WebViewPageScreen import android.net.Uri /** * 应用导航图。 * * 自适应策略: * - **Compact**(< 600dp,手机):单栈 NavHost。guardedPop 时间戳围栏(200ms)防连点栈损坏, * enter/exit/popEnter/popExit 显式使用同一套滑动过渡,避免默认淡入淡出与返回动画混用。 * - **Expanded**(≥ 840dp,平板横屏):左侧 Dashboard + 右侧 when 分支原子切换, * 用量子路由用局部 NavHost。 */ object Routes { const val DASHBOARD = "dashboard" const val SETTINGS = "settings" const val API_MANAGEMENT = "api_management" const val TIPS = "tips" const val DEBUG_LOG = "debug_log" const val CODEX_OAUTH = "codex_oauth" const val CREDENTIAL_EDIT = "credential_edit/{type}?account={account}" fun credentialEdit(type: ServiceType, accountId: String? = null): String = if (accountId.isNullOrBlank()) { "credential_edit/${type.name}" } else { "credential_edit/${type.name}?account=${Uri.encode(accountId)}" } const val ACCOUNTS = "accounts/{type}" fun accounts(type: ServiceType) = "accounts/${type.name}" const val WEBVIEW_LOGIN = "webview_login/{type}?account={account}" fun webviewLogin(type: ServiceType, accountId: String? = null): String = if (accountId.isNullOrBlank()) { "webview_login/${type.name}" } else { "webview_login/${type.name}?account=${Uri.encode(accountId)}" } const val WEBVIEW_PAGE = "webview_page?url={url}&title={title}" fun webviewPage(url: String, title: String) = "webview_page?url=${Uri.encode(url)}&title=${Uri.encode(title)}" const val SERVICE_DETAIL = "service/{type}?account={account}" fun serviceDetail(type: ServiceType, accountId: String? = null): String = if (accountId.isNullOrBlank()) { "service/${type.name}" } else { "service/${type.name}?account=${Uri.encode(accountId)}" } const val USAGE_DETAIL = "usage_detail" const val USAGE_OVERVIEW = "usage_overview" const val USAGE_DATA = "usage_data" const val CCGO_USAGE_DETAIL = "ccgo_usage_detail" const val CCGO_USAGE_OVERVIEW = "ccgo_usage_overview" const val CCGO_USAGE_DATA = "ccgo_usage_data" const val HEATMAP = "heatmap" } private fun parseServiceType(typeName: String?): ServiceType = typeName?.let { ServiceType.fromStorageKey(it) ?: runCatching { ServiceType.valueOf(it) }.getOrNull() } ?: ServiceType.DEEPSEEK /** 凭据入口:多账号服务先进账号管理页,其余直达凭据编辑页。 */ private fun credentialEntry(type: ServiceType): String = if (ServiceType.isMultiAccount(type)) Routes.accounts(type) else Routes.credentialEdit(type) private sealed class DetailPane { object Empty : DetailPane() data class ServiceDetail(val type: ServiceType, val accountId: String? = null) : DetailPane() object OCGOUsage : DetailPane() object CCGOUsage : DetailPane() object Settings : DetailPane() object ApiManagement : DetailPane() object Heatmap : DetailPane() /** 通用外部页面(充值/订阅页),App 内 WebView 打开。 */ data class WebPage(val url: String, val title: String) : DetailPane() } @Composable fun RainyTokenNavHost() { val windowSize = rememberWindowSizeClass() val isExpanded = windowSize.widthSizeClass == androidx.compose.material3.windowsizeclass.WindowWidthSizeClass.Expanded if (isExpanded) { ExpandedLayout() } else { CompactNavHost() } } // ═══════════════════════════════════════════════ // Compact:单栈 NavHost —— guardedPop 围栏 + popExit/popEnter=None // ═══════════════════════════════════════════════ /** * 非 Compose State 的时间戳围栏。 * * 为什么不用 mutableStateOf: * mutableStateOf 写入会触发 CompactNavHost → NavHost 重组。若重组发生在 * AnimatedContent 过渡动画期间,会干扰内部 MutableTransitionState 状态机, * 导致旧 composable 已移除、新 composable 进入动画被中断后未正确启动 → 空白页。 * * 连点保护:cooldownMs 内只放行一次 pop。popBackStack() 返回 false 时 reset。 */ private class PopGuard( private val cooldownMs: Long = 200 ) { private var lastPopTime: Long = 0L fun tryAcquire(): Boolean { val now = System.currentTimeMillis() if (now - lastPopTime < cooldownMs) return false lastPopTime = now return true } fun reset() { lastPopTime = 0L } } @Composable private fun CompactNavHost() { val navController = rememberNavController() // 围栏:200ms 内只允许一次 pop;普通对象持有时间戳,不触发 NavHost 重组 val popGuard = remember { PopGuard() } val guardedPop: () -> Unit = { // 起始页无 previousBackStackEntry,直接跳过,避免无意义的 popBackStack() 调用 if (navController.previousBackStackEntry != null && popGuard.tryAcquire()) { // popBackStack() 返回 false = 已在起始页,reset 围栏不浪费冷却 if (!navController.popBackStack()) { popGuard.reset() } } } NavHost( navController = navController, startDestination = Routes.DASHBOARD, enterTransition = { slideIntoContainer( AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(220) ) + fadeIn(animationSpec = tween(90)) }, exitTransition = { slideOutOfContainer( AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(220) ) + fadeOut(animationSpec = tween(90)) }, // Navigation Compose 会把 pop 过渡绑定到 Android 13+ predictive back 手势进度, // 因此这里使用同一套滑动动画,避免默认淡入淡出与返回动画混用。 popExitTransition = { slideOutOfContainer( AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(220) ) + fadeOut(animationSpec = tween(90)) }, popEnterTransition = { slideIntoContainer( AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(220) ) + fadeIn(animationSpec = tween(90)) } ) { composable(Routes.DASHBOARD) { val rechargeLabel = stringResource(R.string.action_recharge) DashboardScreen( onOpenSettings = { navController.navigate(Routes.SETTINGS) }, onOpenApiManagement = { navController.navigate(Routes.API_MANAGEMENT) }, onOpenService = { type, accountId -> navController.navigate(Routes.serviceDetail(type, accountId)) }, onOpenUsageDetail = { navController.navigate(Routes.USAGE_DETAIL) }, onOpenCcgoUsageDetail = { navController.navigate(Routes.CCGO_USAGE_DETAIL) }, onOpenHeatmap = { navController.navigate(Routes.HEATMAP) }, onOpenRecharge = { type -> ServiceConfigProvider.get(type).externalUrl?.let { url -> navController.navigate(Routes.webviewPage(url, rechargeLabel)) } } ) } composable(Routes.API_MANAGEMENT) { val rechargeLabel = stringResource(R.string.action_recharge) ApiManagementScreen( onBack = guardedPop, onConfigure = { type -> navController.navigate(credentialEntry(type)) }, onRecharge = { type -> ServiceConfigProvider.get(type).externalUrl?.let { url -> navController.navigate(Routes.webviewPage(url, rechargeLabel)) } } ) } composable(Routes.USAGE_DETAIL) { UsageDetailScreen( onBack = guardedPop, onOpenOverview = { navController.navigate(Routes.USAGE_OVERVIEW) }, onOpenData = { navController.navigate(Routes.USAGE_DATA) } ) } composable(Routes.USAGE_OVERVIEW) { UsageOverviewScreen( onBack = guardedPop ) } composable(Routes.USAGE_DATA) { UsageDataScreen( onBack = guardedPop ) } composable(Routes.CCGO_USAGE_DETAIL) { val wid = CommandCodeUsageRepository.CCGO_WORKSPACE_ID val chartVm: UsageChartViewModel = hiltViewModel(key = "ccgo_chart_$wid") val usageVm: UsageViewModel = hiltViewModel(key = "ccgo_$wid") LaunchedEffect(Unit) { usageVm.setWorkspace(wid) chartVm.setWorkspace(wid) } UsageDetailScreen( onBack = guardedPop, onOpenOverview = { navController.navigate(Routes.CCGO_USAGE_OVERVIEW) }, onOpenData = { navController.navigate(Routes.CCGO_USAGE_DATA) }, viewModel = chartVm, clearViewModel = usageVm ) } composable(Routes.CCGO_USAGE_OVERVIEW) { val key = "ccgo_${CommandCodeUsageRepository.CCGO_WORKSPACE_ID}" val ovVm: UsageViewModel = hiltViewModel(key = key) LaunchedEffect(Unit) { ovVm.setWorkspace(CommandCodeUsageRepository.CCGO_WORKSPACE_ID) } UsageOverviewScreen( onBack = guardedPop, viewModel = ovVm, autoLoad = false ) } composable(Routes.CCGO_USAGE_DATA) { val key = "ccgo_${CommandCodeUsageRepository.CCGO_WORKSPACE_ID}" val dataVm: UsageDataViewModel = hiltViewModel(key = key) LaunchedEffect(Unit) { dataVm.setWorkspace(CommandCodeUsageRepository.CCGO_WORKSPACE_ID) } UsageDataScreen( onBack = guardedPop, viewModel = dataVm, autoLoad = false ) } composable(Routes.SETTINGS) { SettingsScreen( onBack = guardedPop, onEditCredential = { type -> navController.navigate(credentialEntry(type)) }, onManageAccounts = { type -> navController.navigate(Routes.accounts(type)) }, onOpenTips = { navController.navigate(Routes.TIPS) }, onOpenDebugLog = { navController.navigate(Routes.DEBUG_LOG) } ) } composable(Routes.ACCOUNTS) { backStackEntry -> val type = parseServiceType(backStackEntry.arguments?.getString("type")) AccountManagementScreen( service = type, onBack = guardedPop, onEditAccount = { svc, accountId -> navController.navigate(Routes.credentialEdit(svc, accountId)) } ) } composable(Routes.TIPS) { TipsScreen(onBack = guardedPop) } composable(Routes.DEBUG_LOG) { DebugLogScreen(onBack = guardedPop) } composable(Routes.CODEX_OAUTH) { CodexOAuthScreen( onBack = guardedPop, onSuccess = guardedPop ) } composable( route = Routes.CREDENTIAL_EDIT, arguments = listOf( navArgument("type") { type = NavType.StringType }, navArgument("account") { type = NavType.StringType nullable = true defaultValue = null } ) ) { backStackEntry -> val type = parseServiceType(backStackEntry.arguments?.getString("type")) val accountId = backStackEntry.arguments?.getString("account") CredentialEditScreen( service = type, accountId = accountId, onBack = guardedPop, onStartWebViewLogin = { svc, acc -> navController.navigate(Routes.webviewLogin(svc, acc)) }, onWebViewLoginSuccess = { }, onStartCodexOAuth = { navController.navigate(Routes.CODEX_OAUTH) } ) } composable( route = Routes.WEBVIEW_LOGIN, arguments = listOf( navArgument("type") { type = NavType.StringType }, navArgument("account") { type = NavType.StringType nullable = true defaultValue = null } ) ) { backStackEntry -> val type = parseServiceType(backStackEntry.arguments?.getString("type")) val accountId = backStackEntry.arguments?.getString("account") WebViewLoginScreen( service = type, accountId = accountId, onBack = guardedPop, onLoginSucceeded = { _, effectiveId -> // 把实际写入的账号 ID 回传给上一层(凭据编辑页),新建账号模式用于回填 if (!effectiveId.isNullOrBlank()) { navController.previousBackStackEntry?.savedStateHandle?.set( CredentialEditViewModel.KEY_LOGIN_ACCOUNT_ID, effectiveId ) } // 登录成功后返回上一层(详情页或凭据编辑页),由 ON_RESUME 刷新内容 if (!navController.popBackStack(Routes.SERVICE_DETAIL, inclusive = false)) { guardedPop() } } ) } composable( route = Routes.SERVICE_DETAIL, arguments = listOf( navArgument("type") { type = NavType.StringType }, navArgument("account") { type = NavType.StringType nullable = true defaultValue = null } ) ) { backStackEntry -> val type = parseServiceType(backStackEntry.arguments?.getString("type")) val accountId = backStackEntry.arguments?.getString("account") val rechargeLabel = stringResource(R.string.action_recharge) ServiceDetailScreen( service = type, accountId = accountId, onBack = guardedPop, onSwitchAccount = { svc, acc -> // 切换账号时替换当前详情页(避免同路由栈堆积) navController.navigate(Routes.serviceDetail(svc, acc)) { popUpTo(Routes.SERVICE_DETAIL) { inclusive = true } } }, onConfigureCredential = { svc -> navController.navigate(credentialEntry(svc)) }, onStartWebViewLogin = { svc, acc -> navController.navigate(Routes.webviewLogin(svc, acc)) }, onOpenRecharge = { svc -> ServiceConfigProvider.get(svc).externalUrl?.let { url -> navController.navigate(Routes.webviewPage(url, rechargeLabel)) } } ) } composable( route = Routes.WEBVIEW_PAGE, arguments = listOf( navArgument("url") { type = NavType.StringType }, navArgument("title") { type = NavType.StringType } ) ) { backStackEntry -> val url = backStackEntry.arguments?.getString("url").orEmpty() val title = backStackEntry.arguments?.getString("title").orEmpty() WebViewPageScreen(url = url, title = title, onBack = guardedPop) } composable(Routes.HEATMAP) { HeatmapScreen(onBack = guardedPop) } } } // ═══════════════════════════════════════════════ // Expanded:手动双窗格 // ═══════════════════════════════════════════════ @Composable private fun ExpandedLayout() { var detailPane by remember { mutableStateOf(DetailPane.Empty) } val dashboardVm: DashboardViewModel = hiltViewModel() // 宽屏布局下 DashboardScreen 永久挂载,不会收到 ON_RESUME。 // 当详情面板关闭时手动触发 Dashboard 刷新凭据状态。 var prevPane by remember { mutableStateOf(DetailPane.Empty) } LaunchedEffect(detailPane) { if (prevPane != DetailPane.Empty && detailPane == DetailPane.Empty) { dashboardVm.reloadLocalState() } prevPane = detailPane } val rechargeLabel = stringResource(R.string.action_recharge) Row(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier .weight(0.35f) .fillMaxHeight() ) { DashboardScreen( onOpenSettings = { detailPane = DetailPane.Settings }, onOpenApiManagement = { detailPane = DetailPane.ApiManagement }, onOpenService = { type, accountId -> detailPane = DetailPane.ServiceDetail(type, accountId) }, onOpenUsageDetail = { detailPane = DetailPane.OCGOUsage }, onOpenCcgoUsageDetail = { detailPane = DetailPane.CCGOUsage }, onOpenHeatmap = { detailPane = DetailPane.Heatmap }, onOpenRecharge = { type -> ServiceConfigProvider.get(type).externalUrl?.let { url -> detailPane = DetailPane.WebPage(url = url, title = rechargeLabel) } }, viewModel = dashboardVm ) } VerticalDivider( thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant ) Box( modifier = Modifier .weight(0.65f) .fillMaxHeight() .background(MaterialTheme.colorScheme.background) ) { ExpandedDetailPane( pane = detailPane, onClose = { detailPane = DetailPane.Empty }, onSwitchAccount = { svc, acc -> detailPane = DetailPane.ServiceDetail(svc, acc) }, onCredentialEditClosed = { dashboardVm.reloadLocalState() } ) } } } @Composable private fun ExpandedDetailPane( pane: DetailPane, onClose: () -> Unit, onSwitchAccount: (ServiceType, String?) -> Unit = { _, _ -> }, onCredentialEditClosed: () -> Unit = {} ) { when (pane) { is DetailPane.Empty -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(stringResource(R.string.nav_empty_title), style = MaterialTheme.typography.titleMedium, color = inkMuted()) Text(stringResource(R.string.nav_empty_subtitle), style = MaterialTheme.typography.bodySmall, color = inkMuted()) } } } is DetailPane.ServiceDetail -> { // key(pane.type + accountId):切换服务/账号时重建嵌套导航栈,避免停留在上一个页面 key("${pane.type.name}:${pane.accountId.orEmpty()}") { val navController = rememberNavController() NavHost( navController = navController, startDestination = "detail", popExitTransition = { ExitTransition.None } ) { composable("detail") { val rechargeLabel = LocalContext.current.getString(R.string.action_recharge) ServiceDetailScreen( service = pane.type, accountId = pane.accountId, onBack = onClose, onSwitchAccount = onSwitchAccount, onConfigureCredential = { onClose() }, onStartWebViewLogin = { _, _ -> }, onOpenRecharge = { svc -> ServiceConfigProvider.get(svc).externalUrl?.let { url -> navController.navigate(Routes.webviewPage(url, rechargeLabel)) } } ) } composable( route = Routes.WEBVIEW_PAGE, arguments = listOf( navArgument("url") { type = NavType.StringType }, navArgument("title") { type = NavType.StringType } ) ) { entry -> WebViewPageScreen( url = entry.arguments?.getString("url").orEmpty(), title = entry.arguments?.getString("title").orEmpty(), onBack = { navController.popBackStack() } ) } } } } is DetailPane.OCGOUsage -> { val navController = rememberNavController() NavHost( navController = navController, startDestination = "chart", popExitTransition = { ExitTransition.None } ) { composable("chart") { UsageDetailScreen( onBack = onClose, onOpenOverview = { navController.navigate("overview") }, onOpenData = { navController.navigate("data") } ) } composable("overview") { UsageOverviewScreen(onBack = { navController.popBackStack() }, autoLoad = true) } composable("data") { UsageDataScreen(onBack = { navController.popBackStack() }, autoLoad = true) } } } is DetailPane.CCGOUsage -> { val navController = rememberNavController() val wid = CommandCodeUsageRepository.CCGO_WORKSPACE_ID val chartVm: UsageChartViewModel = hiltViewModel(key = "ccgo_chart_$wid") val usageVm: UsageViewModel = hiltViewModel(key = "ccgo_$wid") LaunchedEffect(Unit) { usageVm.setWorkspace(wid) chartVm.setWorkspace(wid) } NavHost( navController = navController, startDestination = "chart", popExitTransition = { ExitTransition.None } ) { composable("chart") { UsageDetailScreen( onBack = onClose, onOpenOverview = { navController.navigate("overview") }, onOpenData = { navController.navigate("data") }, viewModel = chartVm, clearViewModel = usageVm ) } composable("overview") { val key = "ccgo_$wid" val ovVm: UsageViewModel = hiltViewModel(key = key) LaunchedEffect(Unit) { ovVm.setWorkspace(wid) } UsageOverviewScreen(onBack = { navController.popBackStack() }, viewModel = ovVm, autoLoad = false) } composable("data") { val key = "ccgo_$wid" val dataVm: UsageDataViewModel = hiltViewModel(key = key) LaunchedEffect(Unit) { dataVm.setWorkspace(wid) } UsageDataScreen(onBack = { navController.popBackStack() }, viewModel = dataVm, autoLoad = false) } } } is DetailPane.Heatmap -> { HeatmapScreen(onBack = onClose) } is DetailPane.WebPage -> { WebViewPageScreen( url = pane.url, title = pane.title, onBack = onClose ) } is DetailPane.ApiManagement -> { val apiNavController = rememberNavController() // 从凭据编辑/WebView 返回到 api_mgmt_main 时刷新 Dashboard 凭据状态 val routeStack by apiNavController.currentBackStackEntryAsState() val currentRoute = routeStack?.destination?.route var prevRoute by remember { mutableStateOf("api_mgmt_main") } LaunchedEffect(currentRoute) { if (prevRoute != currentRoute && currentRoute == "api_mgmt_main") { onCredentialEditClosed() } prevRoute = currentRoute ?: "api_mgmt_main" } NavHost( navController = apiNavController, startDestination = "api_mgmt_main", popExitTransition = { ExitTransition.None } ) { composable("api_mgmt_main") { val rechargeLabel = LocalContext.current.getString(R.string.action_recharge) ApiManagementScreen( onBack = onClose, onConfigure = { type -> apiNavController.navigate(credentialEntry(type)) }, onRecharge = { type -> ServiceConfigProvider.get(type).externalUrl?.let { url -> apiNavController.navigate(Routes.webviewPage(url, rechargeLabel)) } } ) } composable(Routes.ACCOUNTS) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) AccountManagementScreen( service = type, onBack = { apiNavController.popBackStack() }, onEditAccount = { svc, accountId -> apiNavController.navigate(Routes.credentialEdit(svc, accountId)) } ) } composable( route = Routes.CREDENTIAL_EDIT, arguments = listOf( navArgument("type") { type = NavType.StringType }, navArgument("account") { type = NavType.StringType nullable = true defaultValue = null } ) ) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) val accountId = entry.arguments?.getString("account") CredentialEditScreen( service = type, accountId = accountId, onBack = { apiNavController.popBackStack() }, onStartWebViewLogin = { svc, acc -> apiNavController.navigate(Routes.webviewLogin(svc, acc)) }, onWebViewLoginSuccess = { }, onStartCodexOAuth = { apiNavController.navigate(Routes.CODEX_OAUTH) } ) } composable( route = Routes.WEBVIEW_LOGIN, arguments = listOf( navArgument("type") { type = NavType.StringType }, navArgument("account") { type = NavType.StringType nullable = true defaultValue = null } ) ) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) val accountId = entry.arguments?.getString("account") WebViewLoginScreen( service = type, accountId = accountId, onBack = { apiNavController.popBackStack() }, onLoginSucceeded = { _, effectiveId -> if (!effectiveId.isNullOrBlank()) { apiNavController.previousBackStackEntry?.savedStateHandle?.set( CredentialEditViewModel.KEY_LOGIN_ACCOUNT_ID, effectiveId ) } apiNavController.popBackStack() } ) } composable(Routes.CODEX_OAUTH) { CodexOAuthScreen( onBack = { apiNavController.popBackStack() }, onSuccess = { apiNavController.popBackStack() } ) } composable( route = Routes.WEBVIEW_PAGE, arguments = listOf( navArgument("url") { type = NavType.StringType }, navArgument("title") { type = NavType.StringType } ) ) { entry -> WebViewPageScreen( url = entry.arguments?.getString("url").orEmpty(), title = entry.arguments?.getString("title").orEmpty(), onBack = { apiNavController.popBackStack() } ) } } } is DetailPane.Settings -> { val settingsNavController = rememberNavController() // 监听嵌套 NavHost 路由变化:当从凭据编辑页/WebView 返回 settings_main 时 // 通知 Dashboard 刷新凭据状态(宽屏布局 Dashboard 永久挂载不产生 ON_RESUME) val routeStack by settingsNavController.currentBackStackEntryAsState() val currentRoute = routeStack?.destination?.route var prevRoute by remember { mutableStateOf("settings_main") } LaunchedEffect(currentRoute) { if (prevRoute != currentRoute && currentRoute == "settings_main") { onCredentialEditClosed() } prevRoute = currentRoute ?: "settings_main" } NavHost( navController = settingsNavController, startDestination = "settings_main", popExitTransition = { ExitTransition.None } ) { composable("settings_main") { SettingsScreen( onBack = onClose, onEditCredential = { type -> settingsNavController.navigate(credentialEntry(type)) }, onManageAccounts = { type -> settingsNavController.navigate(Routes.accounts(type)) }, onOpenTips = { settingsNavController.navigate("tips") }, onOpenDebugLog = { settingsNavController.navigate("debug_log") } ) } composable(Routes.ACCOUNTS) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) AccountManagementScreen( service = type, onBack = { settingsNavController.popBackStack() }, onEditAccount = { svc, accountId -> settingsNavController.navigate(Routes.credentialEdit(svc, accountId)) } ) } composable("tips") { TipsScreen(onBack = { settingsNavController.popBackStack() }) } composable("debug_log") { DebugLogScreen(onBack = { settingsNavController.popBackStack() }) } composable(Routes.CODEX_OAUTH) { CodexOAuthScreen( onBack = { settingsNavController.popBackStack() }, onSuccess = { settingsNavController.popBackStack() } ) } composable( route = Routes.CREDENTIAL_EDIT, arguments = listOf( navArgument("type") { type = NavType.StringType }, navArgument("account") { type = NavType.StringType nullable = true defaultValue = null } ) ) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) val accountId = entry.arguments?.getString("account") CredentialEditScreen( service = type, accountId = accountId, onBack = { settingsNavController.popBackStack() }, onStartWebViewLogin = { svc, acc -> settingsNavController.navigate(Routes.webviewLogin(svc, acc)) }, onWebViewLoginSuccess = { }, onStartCodexOAuth = { settingsNavController.navigate(Routes.CODEX_OAUTH) } ) } composable( route = Routes.WEBVIEW_LOGIN, arguments = listOf( navArgument("type") { type = NavType.StringType }, navArgument("account") { type = NavType.StringType nullable = true defaultValue = null } ) ) { entry -> val type = parseServiceType(entry.arguments?.getString("type")) val accountId = entry.arguments?.getString("account") WebViewLoginScreen( service = type, accountId = accountId, onBack = { settingsNavController.popBackStack() }, onLoginSucceeded = { _, effectiveId -> if (!effectiveId.isNullOrBlank()) { settingsNavController.previousBackStackEntry?.savedStateHandle?.set( CredentialEditViewModel.KEY_LOGIN_ACCOUNT_ID, effectiveId ) } settingsNavController.popBackStack() } ) } } } } }