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:
parent
dc3027417e
commit
78f93588f8
@ -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) }
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user