fix: 详情页新鲜缓存不再标记为缓存,用量卡片显示刷新时间
This commit is contained in:
parent
acddaae068
commit
8340ded070
@ -171,6 +171,16 @@ fun CommandCodeUsageStatsCard(
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 刷新时间(与其余服务卡片底部一致) ───
|
||||
if (uiState.lastSyncAt > 0) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = usageUpdatedAtText(uiState.lastSyncAt),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = InkMuted
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -559,3 +559,20 @@ internal fun footerText(card: DashboardCardUi): UiText {
|
||||
else -> UiText.Resource(R.string.footer_updated_at, listOf(timeStr))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用量卡片(OCGO / CCGO)底部刷新时间文案,规则与 [footerText] 完全一致:
|
||||
* <1 分钟"刚刚更新"、<1 小时"X 分钟前"、<24 小时"X 小时前"、更早显示 "MM-dd HH:mm 更新"。
|
||||
*/
|
||||
@Composable
|
||||
internal fun usageUpdatedAtText(updatedAt: Long): String {
|
||||
val now = System.currentTimeMillis()
|
||||
val diffMin = (now - updatedAt) / 60_000
|
||||
val sdf = SimpleDateFormat("MM-dd HH:mm", Locale.getDefault())
|
||||
return when {
|
||||
diffMin < 1 -> stringResource(R.string.footer_just_updated)
|
||||
diffMin < 60 -> stringResource(R.string.footer_minutes_ago, diffMin)
|
||||
diffMin < 1440 -> stringResource(R.string.footer_hours_ago, diffMin / 60)
|
||||
else -> stringResource(R.string.footer_updated_at, sdf.format(Date(updatedAt)))
|
||||
}
|
||||
}
|
||||
@ -169,6 +169,16 @@ fun UsageStatsCard(
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 刷新时间(与其余服务卡片底部一致) ───
|
||||
if (uiState.lastSyncAt > 0) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = usageUpdatedAtText(uiState.lastSyncAt),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = InkMuted
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +88,22 @@ class UsageViewModel @Inject constructor(
|
||||
private var workspaceIdOverride: String? = null
|
||||
private var loadGeneration = 0 // 递增:过时的 loadStatsInternal 结果自动丢弃
|
||||
|
||||
/** 用量同步时间持久化(按 workspace 区分 OCGO / CCGO),重启后卡片仍能显示刷新时间。 */
|
||||
private fun syncAtPrefs(): android.content.SharedPreferences =
|
||||
com.rainy.token.RainyTokenApplication.appContext.getSharedPreferences(
|
||||
"usage_sync_at",
|
||||
android.content.Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
private fun syncAtKey(wid: String): String = "sync_at_$wid"
|
||||
|
||||
private fun loadLastSyncAt(wid: String): Long =
|
||||
syncAtPrefs().getLong(syncAtKey(wid), 0L)
|
||||
|
||||
private fun saveLastSyncAt(wid: String, ts: Long) {
|
||||
syncAtPrefs().edit().putLong(syncAtKey(wid), ts).apply()
|
||||
}
|
||||
|
||||
/** 覆盖 workspaceId,用于 CCGO 等非 OCGO 服务。必须在 loadStats() 前调用。 */
|
||||
fun setWorkspace(wid: String) {
|
||||
workspaceIdOverride = wid
|
||||
@ -158,7 +174,8 @@ class UsageViewModel @Inject constructor(
|
||||
dailyStats = dailyStats,
|
||||
recordCount = totalCount,
|
||||
loading = false,
|
||||
dailyPage = 1
|
||||
dailyPage = 1,
|
||||
lastSyncAt = loadLastSyncAt(wid)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -189,6 +206,7 @@ class UsageViewModel @Inject constructor(
|
||||
fun sync() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(syncing = true) }
|
||||
val wid = workspaceIdOverride ?: workspaceId()
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
val cache = cacheProvider.get()
|
||||
if (workspaceIdOverride == com.rainy.token.data.repository.CommandCodeUsageRepository.CCGO_WORKSPACE_ID) {
|
||||
@ -201,12 +219,15 @@ class UsageViewModel @Inject constructor(
|
||||
if (count == 0) useCase.fullSync() else useCase.incrementalSync()
|
||||
}
|
||||
}
|
||||
val syncedAt = System.currentTimeMillis()
|
||||
if (result.isSuccess && wid != null) saveLastSyncAt(wid, syncedAt)
|
||||
result.onSuccess { loadStats() }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
syncing = false,
|
||||
lastSyncResult = result.getOrNull()?.inserted ?: 0,
|
||||
lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) }
|
||||
lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) },
|
||||
lastSyncAt = if (result.isSuccess && wid != null) syncedAt else it.lastSyncAt
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -222,12 +243,15 @@ class UsageViewModel @Inject constructor(
|
||||
invalidateData()
|
||||
val useCase = syncCommandCodeUseCaseProvider.get()
|
||||
val result = useCase.fullSync()
|
||||
val syncedAt = System.currentTimeMillis()
|
||||
if (result.isSuccess) saveLastSyncAt(wid, syncedAt)
|
||||
result.onSuccess { loadStats() }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
syncing = false,
|
||||
lastSyncResult = result.getOrNull()?.inserted ?: 0,
|
||||
lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) }
|
||||
lastSyncError = result.exceptionOrNull()?.let { syncErrorToUiText(it) },
|
||||
lastSyncAt = if (result.isSuccess) syncedAt else it.lastSyncAt
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -247,6 +271,7 @@ data class UsageUiState(
|
||||
val recordCount: Int = 0,
|
||||
val lastSyncResult: Int = 0,
|
||||
val lastSyncError: UiText? = null,
|
||||
val lastSyncAt: Long = 0, // 最近一次成功同步的 epoch ms(0 = 从未同步)
|
||||
val timeFilter: TimeFilter = TimeFilter.All,
|
||||
val dailyPage: Int = 1,
|
||||
val modelFilter: String? = null // null = 全部模型
|
||||
|
||||
@ -65,6 +65,9 @@ class ServiceDetailViewModel @Inject constructor(
|
||||
private const val OCGO_PREFS = "ocgo_trigger_prefs"
|
||||
private const val OLLAMA_PREFS = "ollama_trigger_prefs"
|
||||
|
||||
/** 缓存新鲜度阈值:fetchedAt 距今 ≤5 分钟视为最新数据,不再标记为"缓存"。 */
|
||||
private const val CACHE_FRESH_THRESHOLD_MS = 5 * 60_000L
|
||||
|
||||
internal fun credentialFingerprint(credential: Credential?): String? =
|
||||
CredentialRepository.credentialFingerprint(credential)
|
||||
|
||||
@ -441,7 +444,12 @@ class ServiceDetailViewModel @Inject constructor(
|
||||
RepositoryError.InvalidCredential()
|
||||
)
|
||||
cached != null && status.state == CredentialStatus.State.OK ->
|
||||
// 缓存新鲜(≤5 分钟)时视为最新数据直接展示,避免"刚刷新完进详情却显示缓存"的困惑
|
||||
if (isCacheFresh(cached.fetchedAt)) {
|
||||
State.Fresh(cached.balance)
|
||||
} else {
|
||||
State.Stale(cached.balance, cached.fetchedAt)
|
||||
}
|
||||
cached != null ->
|
||||
State.Error(
|
||||
cached.balance,
|
||||
@ -460,7 +468,11 @@ class ServiceDetailViewModel @Inject constructor(
|
||||
state = newState
|
||||
)
|
||||
}
|
||||
if (!isManual && status.state != CredentialStatus.State.NOT_CONFIGURED && cached == null) {
|
||||
// 无缓存或缓存已过期(>5 分钟)时自动刷新一次;
|
||||
// 缓存新鲜时直接展示,避免每次进详情页都发网络请求
|
||||
if (!isManual && status.state != CredentialStatus.State.NOT_CONFIGURED &&
|
||||
(cached == null || !isCacheFresh(cached.fetchedAt))
|
||||
) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
@ -486,10 +498,19 @@ class ServiceDetailViewModel @Inject constructor(
|
||||
cacheAdvanced && current.state is State.Fresh ->
|
||||
State.Fresh(mergedCache!!.balance)
|
||||
cacheAdvanced && current.state is State.Stale ->
|
||||
State.Stale(mergedCache!!.balance, mergedCache.fetchedAt)
|
||||
if (isCacheFresh(mergedCache!!.fetchedAt)) {
|
||||
State.Fresh(mergedCache.balance)
|
||||
} else {
|
||||
State.Stale(mergedCache.balance, mergedCache.fetchedAt)
|
||||
}
|
||||
current.state is State.Fresh -> current.state
|
||||
current.state is State.Stale -> current.state
|
||||
mergedCache != null -> State.Stale(mergedCache.balance, mergedCache.fetchedAt)
|
||||
mergedCache != null ->
|
||||
if (isCacheFresh(mergedCache.fetchedAt)) {
|
||||
State.Fresh(mergedCache.balance)
|
||||
} else {
|
||||
State.Stale(mergedCache.balance, mergedCache.fetchedAt)
|
||||
}
|
||||
else -> current.state
|
||||
}
|
||||
return current.copy(
|
||||
@ -514,6 +535,10 @@ class ServiceDetailViewModel @Inject constructor(
|
||||
current: CachedBalance?
|
||||
): Boolean = candidate != null && (current == null || candidate.fetchedAt > current.fetchedAt)
|
||||
|
||||
/** 缓存是否仍算"最新":fetchedAt 距今 ≤5 分钟(与 Dashboard 底部"刚刚更新"感知一致)。 */
|
||||
private fun isCacheFresh(fetchedAt: Long): Boolean =
|
||||
System.currentTimeMillis() - fetchedAt <= CACHE_FRESH_THRESHOLD_MS
|
||||
|
||||
private fun isStaleRequest(type: ServiceType, serviceGen: Int, refreshGen: Int): Boolean =
|
||||
refreshGen != refreshGeneration ||
|
||||
serviceGen != serviceGeneration ||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user