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 panelWindowStats by viewModel.panelWindowStats.collectAsStateWithLifecycle()
val panelKeyStats by viewModel.panelKeyStats.collectAsStateWithLifecycle() val panelKeyStats by viewModel.panelKeyStats.collectAsStateWithLifecycle()
val hourlyTrend by viewModel.hourlyTrend.collectAsStateWithLifecycle()
val config = ServiceConfigProvider.get(service) val config = ServiceConfigProvider.get(service)
val isManualMode = config.method == FetchMethod.MANUAL val isManualMode = config.method == FetchMethod.MANUAL
@ -207,6 +208,8 @@ fun ServiceDetailScreen(
state = uiState.state, state = uiState.state,
windowStatsState = panelWindowStats, windowStatsState = panelWindowStats,
onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) }, onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) },
hourlyTrend = hourlyTrend,
onLoadHourlyTrend = { date -> viewModel.loadHourlyTrend(date) },
keyStatsState = panelKeyStats, keyStatsState = panelKeyStats,
onLoadKeyStats = { from, to -> viewModel.loadPanelKeyStats(from, to) } 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.Credential
import com.rainy.token.domain.model.CredentialStatus import com.rainy.token.domain.model.CredentialStatus
import com.rainy.token.domain.model.ServiceBalance 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.domain.model.TriggerSummary
import com.rainy.token.R import com.rainy.token.R
import com.rainy.token.domain.service.ServiceType 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」用量状态(面板使用记录口径,按当前时间周期)。 */ /** 「按 Key」用量状态(面板使用记录口径,按当前时间周期)。 */
private val _panelKeyStats = MutableStateFlow<PanelKeyStatsState>(PanelKeyStatsState.Idle) private val _panelKeyStats = MutableStateFlow<PanelKeyStatsState>(PanelKeyStatsState.Idle)
val panelKeyStats: StateFlow<PanelKeyStatsState> = _panelKeyStats.asStateFlow() val panelKeyStats: StateFlow<PanelKeyStatsState> = _panelKeyStats.asStateFlow()

View File

@ -1,14 +1,18 @@
package com.rainy.token.ui.servicedetail package com.rainy.token.ui.servicedetail
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -20,6 +24,7 @@ import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -29,6 +34,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -66,7 +72,7 @@ internal fun Sub2ApiUsageCard(
onLoadWindowModelStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null, onLoadWindowModelStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null,
/** 「今天」窗口的按小时趋势;非空时用小时柱替代按天柱(按天只有一根柱没有信息量)。 */ /** 「今天」窗口的按小时趋势;非空时用小时柱替代按天柱(按天只有一根柱没有信息量)。 */
hourlyTrend: List<Sub2DailyUsage> = emptyList(), hourlyTrend: List<Sub2DailyUsage> = emptyList(),
onLoadHourlyTrend: (() -> Unit)? = null, onLoadHourlyTrend: ((LocalDate) -> Unit)? = null,
/** 「按 Key」tab 的数据状态(按当前时间周期查询面板使用记录)。 */ /** 「按 Key」tab 的数据状态(按当前时间周期查询面板使用记录)。 */
keyStatsState: PanelKeyStatsState = PanelKeyStatsState.Idle, keyStatsState: PanelKeyStatsState = PanelKeyStatsState.Idle,
onLoadKeyStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null onLoadKeyStats: ((from: LocalDate, to: LocalDate) -> Unit)? = null
@ -109,7 +115,7 @@ internal fun Sub2ApiUsageCard(
// key=detail:刷新后模型列表变化时重置选中态,避免「每日区隐藏+无模型说明」的悬空状态 // key=detail:刷新后模型列表变化时重置选中态,避免「每日区隐藏+无模型说明」的悬空状态
var selectedModel by remember(detail) { mutableStateOf<String?>(null) } var selectedModel by remember(detail) { mutableStateOf<String?>(null) }
// 明细分区 tab:按模型 / 按 Key // 明细分区 tab:按模型 / 按 Key
var detailTab by remember(detail) { mutableStateOf(DetailTab.MODEL) } var detailTab by remember { mutableStateOf(DetailTab.MODEL) }
// ── 时间窗口选择 ── // ── 时间窗口选择 ──
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
@ -131,7 +137,10 @@ internal fun Sub2ApiUsageCard(
// 「今天」改为按小时展示:只有一根按天柱子看不出分布,故拉当天小时趋势 // 「今天」改为按小时展示:只有一根按天柱子看不出分布,故拉当天小时趋势
LaunchedEffect(window) { LaunchedEffect(window) {
if (window == Sub2Window.TODAY) onLoadHourlyTrend?.invoke() if (window == Sub2Window.TODAY) {
val (from, _) = window.range()
onLoadHourlyTrend?.invoke(from)
}
} }
// 小时数据只在「今天」窗口且拿到 ≥2 个小时桶时才用。 // 小时数据只在「今天」窗口且拿到 ≥2 个小时桶时才用。
// 实测面板可能把整天聚合成单个 "00:00" 桶(时区/活动量原因), // 实测面板可能把整天聚合成单个 "00:00" 桶(时区/活动量原因),
@ -292,27 +301,19 @@ internal fun Sub2ApiUsageCard(
if (detail.modelStats.isNotEmpty() || window != Sub2Window.ALL) { if (detail.modelStats.isNotEmpty() || window != Sub2Window.ALL) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
Spacer(modifier = Modifier.height(14.dp)) Spacer(modifier = Modifier.height(10.dp))
// tab 切换:按模型 / 按 Key // tab 切换:按模型 / 按 Key(文字样式,非按钮)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
FilterChip( FilterChip(
selected = detailTab == DetailTab.MODEL, selected = detailTab == DetailTab.MODEL,
onClick = { detailTab = DetailTab.MODEL }, onClick = { detailTab = DetailTab.MODEL },
label = { Text(stringResource(R.string.sub2_tab_by_model)) }, label = { Text(stringResource(R.string.sub2_tab_by_model)) }
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedLabelColor = MaterialTheme.colorScheme.onPrimary
)
) )
FilterChip( FilterChip(
selected = detailTab == DetailTab.KEY, selected = detailTab == DetailTab.KEY,
onClick = { detailTab = DetailTab.KEY }, onClick = { detailTab = DetailTab.KEY },
label = { Text(stringResource(R.string.sub2_tab_by_key)) }, label = { Text(stringResource(R.string.sub2_tab_by_key)) }
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedLabelColor = MaterialTheme.colorScheme.onPrimary
)
) )
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))