diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt index 2b11703..9cc8b97 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailScreen.kt @@ -121,6 +121,7 @@ fun ServiceDetailScreen( // 详情页只保留用量明细所需的窗口统计状态。 val panelWindowStats by viewModel.panelWindowStats.collectAsStateWithLifecycle() val panelKeyStats by viewModel.panelKeyStats.collectAsStateWithLifecycle() + val hourlyTrend by viewModel.hourlyTrend.collectAsStateWithLifecycle() val config = ServiceConfigProvider.get(service) val isManualMode = config.method == FetchMethod.MANUAL @@ -207,6 +208,8 @@ fun ServiceDetailScreen( state = uiState.state, windowStatsState = panelWindowStats, onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) }, + hourlyTrend = hourlyTrend, + onLoadHourlyTrend = { date -> viewModel.loadHourlyTrend(date) }, keyStatsState = panelKeyStats, onLoadKeyStats = { from, to -> viewModel.loadPanelKeyStats(from, to) } ) diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt index 750da93..94225b5 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/ServiceDetailViewModel.kt @@ -12,6 +12,7 @@ import com.rainy.token.data.repository.TriggerErrorReason import com.rainy.token.domain.model.Credential import com.rainy.token.domain.model.CredentialStatus import com.rainy.token.domain.model.ServiceBalance +import com.rainy.token.domain.model.Sub2DailyUsage import com.rainy.token.domain.model.TriggerSummary import com.rainy.token.R import com.rainy.token.domain.service.ServiceType @@ -132,6 +133,21 @@ class ServiceDetailViewModel @Inject constructor( } } + /** 「今天」按小时趋势(面板 snapshot-v2?granularity=hour)。 */ + private val _hourlyTrend = MutableStateFlow>(emptyList()) + val hourlyTrend: StateFlow> = _hourlyTrend.asStateFlow() + + private var hourlyTrendGeneration = 0 + + fun loadHourlyTrend(date: LocalDate) { + val generation = ++hourlyTrendGeneration + viewModelScope.launch { + val result = sub2ApiRepository.fetchHourlyTrend(date) + if (generation != hourlyTrendGeneration) return@launch + _hourlyTrend.value = result.getOrDefault(emptyList()) + } + } + /** 「按 Key」用量状态(面板使用记录口径,按当前时间周期)。 */ private val _panelKeyStats = MutableStateFlow(PanelKeyStatsState.Idle) val panelKeyStats: StateFlow = _panelKeyStats.asStateFlow() diff --git a/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt b/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt index b7452de..10e3a79 100644 --- a/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt +++ b/app/src/main/java/com/rainy/token/ui/servicedetail/Sub2ApiUsageCard.kt @@ -1,14 +1,18 @@ package com.rainy.token.ui.servicedetail import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items @@ -20,6 +24,7 @@ import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -29,6 +34,7 @@ import androidx.compose.runtime.setValue 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.graphics.Brush import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -66,7 +72,7 @@ internal fun Sub2ApiUsageCard( onLoadWindowModelStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null, /** 「今天」窗口的按小时趋势;非空时用小时柱替代按天柱(按天只有一根柱没有信息量)。 */ hourlyTrend: List = emptyList(), - onLoadHourlyTrend: (() -> Unit)? = null, + onLoadHourlyTrend: ((LocalDate) -> Unit)? = null, /** 「按 Key」tab 的数据状态(按当前时间周期查询面板使用记录)。 */ keyStatsState: PanelKeyStatsState = PanelKeyStatsState.Idle, onLoadKeyStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null @@ -109,7 +115,7 @@ internal fun Sub2ApiUsageCard( // key=detail:刷新后模型列表变化时重置选中态,避免「每日区隐藏+无模型说明」的悬空状态 var selectedModel by remember(detail) { mutableStateOf(null) } // 明细分区 tab:按模型 / 按 Key - var detailTab by remember(detail) { mutableStateOf(DetailTab.MODEL) } + var detailTab by remember { mutableStateOf(DetailTab.MODEL) } // ── 时间窗口选择 ── LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -131,7 +137,10 @@ internal fun Sub2ApiUsageCard( // 「今天」改为按小时展示:只有一根按天柱子看不出分布,故拉当天小时趋势 LaunchedEffect(window) { - if (window == Sub2Window.TODAY) onLoadHourlyTrend?.invoke() + if (window == Sub2Window.TODAY) { + val (from, _) = window.range() + onLoadHourlyTrend?.invoke(from) + } } // 小时数据只在「今天」窗口且拿到 ≥2 个小时桶时才用。 // 实测面板可能把整天聚合成单个 "00:00" 桶(时区/活动量原因), @@ -292,27 +301,19 @@ internal fun Sub2ApiUsageCard( if (detail.modelStats.isNotEmpty() || window != Sub2Window.ALL) { Spacer(modifier = Modifier.height(16.dp)) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(10.dp)) - // tab 切换:按模型 / 按 Key - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + // tab 切换:按模型 / 按 Key(文字样式,非按钮) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { FilterChip( selected = detailTab == DetailTab.MODEL, onClick = { detailTab = DetailTab.MODEL }, - label = { Text(stringResource(R.string.sub2_tab_by_model)) }, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MaterialTheme.colorScheme.primary, - selectedLabelColor = MaterialTheme.colorScheme.onPrimary - ) + label = { Text(stringResource(R.string.sub2_tab_by_model)) } ) FilterChip( selected = detailTab == DetailTab.KEY, onClick = { detailTab = DetailTab.KEY }, - label = { Text(stringResource(R.string.sub2_tab_by_key)) }, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MaterialTheme.colorScheme.primary, - selectedLabelColor = MaterialTheme.colorScheme.onPrimary - ) + label = { Text(stringResource(R.string.sub2_tab_by_key)) } ) } Spacer(modifier = Modifier.height(12.dp))