fix(sub2api): 修复柱状图不显示——接入按小时趋势

详情页「今天」窗口柱状图只有一根按天柱子看不出分布,
原因是 hourlyTrend / onLoadHourlyTrend 参数从未从 Screen 传入 Card。

修复:
- VM 新增 hourlyTrend StateFlow + loadHourlyTrend(date) loader
- Screen 传入 hourlyTrend + onLoadHourlyTrend 回调
- Card 的 onLoadHourlyTrend 改为 (LocalDate) -> Unit,今天窗口时拉取小时趋势
- detailTab 状态去掉 remember(detail) 依赖,避免后台刷新余额时重置 tab 选中
This commit is contained in:
Liuxinyu176 2026-09-13 12:18:11 +08:00
parent dc3027417e
commit 78f93588f8
3 changed files with 36 additions and 16 deletions

View File

@ -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) }
)

View File

@ -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<List<Sub2DailyUsage>>(emptyList())
val hourlyTrend: StateFlow<List<Sub2DailyUsage>> = _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>(PanelKeyStatsState.Idle)
val panelKeyStats: StateFlow<PanelKeyStatsState> = _panelKeyStats.asStateFlow()

View File

@ -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<Sub2DailyUsage> = 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<String?>(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))