feat(heatmap): add HeatmapScreen, navigation route, dashboard entry, i18n strings
- HeatmapScreen with TopAppBar, view switcher (Daily/Weekly/Cumulative), Canvas - DashboardScreen: new HeatmapEntryCard with CalendarMonth icon - RainyTokenNavHost: HEATMAP route for both Compact and Expanded layouts - 9 new string resources across EN/zh-Hans/zh-Hant - ArrowBack -> AutoMirrored.Filled.ArrowBack (deprecation fix)
This commit is contained in:
parent
6e4a5ff9cb
commit
dd91e94805
@ -48,6 +48,7 @@ import com.rainy.token.ui.dashboard.UsageDataViewModel
|
||||
import com.rainy.token.ui.dashboard.UsageDetailScreen
|
||||
import com.rainy.token.ui.dashboard.UsageOverviewScreen
|
||||
import com.rainy.token.ui.dashboard.UsageViewModel
|
||||
import com.rainy.token.ui.heatmap.HeatmapScreen
|
||||
import com.rainy.token.ui.servicedetail.ServiceDetailScreen
|
||||
import com.rainy.token.ui.settings.CredentialEditScreen
|
||||
import com.rainy.token.ui.settings.SettingsScreen
|
||||
@ -83,6 +84,7 @@ object Routes {
|
||||
const val CCGO_USAGE_DETAIL = "ccgo_usage_detail"
|
||||
const val CCGO_USAGE_OVERVIEW = "ccgo_usage_overview"
|
||||
const val CCGO_USAGE_DATA = "ccgo_usage_data"
|
||||
const val HEATMAP = "heatmap"
|
||||
}
|
||||
|
||||
private fun parseServiceType(typeName: String?): ServiceType =
|
||||
@ -96,6 +98,7 @@ private sealed class DetailPane {
|
||||
object OCGOUsage : DetailPane()
|
||||
object CCGOUsage : DetailPane()
|
||||
object Settings : DetailPane()
|
||||
object Heatmap : DetailPane()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -191,7 +194,8 @@ private fun CompactNavHost() {
|
||||
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
||||
onOpenService = { type -> navController.navigate(Routes.serviceDetail(type)) },
|
||||
onOpenUsageDetail = { navController.navigate(Routes.USAGE_DETAIL) },
|
||||
onOpenCcgoUsageDetail = { navController.navigate(Routes.CCGO_USAGE_DETAIL) }
|
||||
onOpenCcgoUsageDetail = { navController.navigate(Routes.CCGO_USAGE_DETAIL) },
|
||||
onOpenHeatmap = { navController.navigate(Routes.HEATMAP) }
|
||||
)
|
||||
}
|
||||
composable(Routes.USAGE_DETAIL) {
|
||||
@ -303,6 +307,9 @@ private fun CompactNavHost() {
|
||||
onStartWebViewLogin = { svc -> navController.navigate(Routes.webviewLogin(svc)) }
|
||||
)
|
||||
}
|
||||
composable(Routes.HEATMAP) {
|
||||
HeatmapScreen(onBack = guardedPop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -336,6 +343,7 @@ private fun ExpandedLayout() {
|
||||
onOpenService = { type -> detailPane = DetailPane.ServiceDetail(type) },
|
||||
onOpenUsageDetail = { detailPane = DetailPane.OCGOUsage },
|
||||
onOpenCcgoUsageDetail = { detailPane = DetailPane.CCGOUsage },
|
||||
onOpenHeatmap = { detailPane = DetailPane.Heatmap },
|
||||
viewModel = dashboardVm
|
||||
)
|
||||
}
|
||||
@ -442,6 +450,9 @@ private fun ExpandedDetailPane(
|
||||
}
|
||||
}
|
||||
}
|
||||
is DetailPane.Heatmap -> {
|
||||
HeatmapScreen(onBack = onClose)
|
||||
}
|
||||
is DetailPane.Settings -> {
|
||||
val settingsNavController = rememberNavController()
|
||||
// 监听嵌套 NavHost 路由变化:当从凭据编辑页/WebView 返回 settings_main 时
|
||||
|
||||
@ -29,6 +29,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
@ -122,6 +123,7 @@ fun DashboardScreen(
|
||||
onOpenService: (ServiceType) -> Unit,
|
||||
onOpenUsageDetail: () -> Unit,
|
||||
onOpenCcgoUsageDetail: () -> Unit = {},
|
||||
onOpenHeatmap: () -> Unit = {},
|
||||
viewModel: DashboardViewModel = hiltViewModel()
|
||||
) {
|
||||
// 从设置页返回时重新读取本地凭据状态 + 缓存(不自动发起网络请求)
|
||||
@ -280,6 +282,7 @@ fun DashboardScreen(
|
||||
onOpenUsageDetail = onOpenUsageDetail,
|
||||
onOpenCcgoUsageDetail = onOpenCcgoUsageDetail,
|
||||
onOpenService = onOpenService,
|
||||
onOpenHeatmap = onOpenHeatmap,
|
||||
refreshTrigger = usageSyncTrigger
|
||||
)
|
||||
DraggableDashboardCards(
|
||||
@ -346,6 +349,7 @@ private const val DASHBOARD_ORDER_KEY = "order"
|
||||
private const val USAGE_OCGO_CARD_ID = "usage:opencode_go"
|
||||
private const val USAGE_CCGO_CARD_ID = "usage:commandcode_go"
|
||||
private const val DASHBOARD_CARD_SPACING_DP = 12
|
||||
private const val HEATMAP_CARD_ID = "heatmap"
|
||||
|
||||
private data class DashboardHomeItem(
|
||||
val id: String,
|
||||
@ -359,6 +363,7 @@ private fun rememberDashboardItems(
|
||||
onOpenUsageDetail: () -> Unit,
|
||||
onOpenCcgoUsageDetail: () -> Unit,
|
||||
onOpenService: (ServiceType) -> Unit,
|
||||
onOpenHeatmap: () -> Unit,
|
||||
refreshTrigger: Int
|
||||
): List<DashboardHomeItem> {
|
||||
val defaultItems = buildList {
|
||||
@ -368,6 +373,9 @@ private fun rememberDashboardItems(
|
||||
add(DashboardHomeItem(USAGE_CCGO_CARD_ID) {
|
||||
CommandCodeUsageStatsCard(onOpenDetail = onOpenCcgoUsageDetail, refreshTrigger = refreshTrigger)
|
||||
})
|
||||
add(DashboardHomeItem(HEATMAP_CARD_ID) {
|
||||
HeatmapEntryCard(onOpenHeatmap = onOpenHeatmap)
|
||||
})
|
||||
cards.forEach { card ->
|
||||
add(DashboardHomeItem("service:${card.service.storageKey}") {
|
||||
DashboardCard(
|
||||
@ -685,6 +693,54 @@ private fun DashboardCard(card: DashboardCardUi, onClick: () -> Unit, onOpenUsag
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 活动热力图入口卡片。
|
||||
* 白底圆角,左侧日历图标,中间标题,右侧箭头,点击跳转热力图页面。
|
||||
*/
|
||||
@Composable
|
||||
private fun HeatmapEntryCard(onOpenHeatmap: () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onOpenHeatmap() },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CalendarMonth,
|
||||
contentDescription = null,
|
||||
tint = StrawberryPink,
|
||||
modifier = Modifier.size(44.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.heatmap_entry),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.heatmap_entry_subtitle),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = inkMuted()
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = StrawberryPink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仪表盘底部 footer:填空白 + 显示版本号。
|
||||
* 容器透明无边框,让卡片列表与背景融合自然。
|
||||
|
||||
110
app/src/main/java/com/rainy/token/ui/heatmap/HeatmapScreen.kt
Normal file
110
app/src/main/java/com/rainy/token/ui/heatmap/HeatmapScreen.kt
Normal file
@ -0,0 +1,110 @@
|
||||
package com.rainy.token.ui.heatmap
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.rainy.token.R
|
||||
import com.rainy.token.ui.theme.inkMuted
|
||||
|
||||
/**
|
||||
* Token 活动热力图页面。
|
||||
*
|
||||
* Scaffold + TopAppBar 布局,包含视图切换器(每日/每周/累计)和 HeatmapCanvas。
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HeatmapScreen(
|
||||
onBack: () -> Unit,
|
||||
viewModel: HeatmapViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.load()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color.Transparent,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.heatmap_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
// ── 视图切换器 ──
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(horizontal = 4.dp, bottom = 12.dp)
|
||||
) {
|
||||
HeatmapViewMode.entries.forEach { mode ->
|
||||
val label = when (mode) {
|
||||
HeatmapViewMode.DAILY -> stringResource(R.string.heatmap_view_daily)
|
||||
HeatmapViewMode.WEEKLY -> stringResource(R.string.heatmap_view_weekly)
|
||||
HeatmapViewMode.CUMULATIVE -> stringResource(R.string.heatmap_view_cumulative)
|
||||
}
|
||||
val isSelected = state.viewMode == mode
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onSurface else inkMuted(),
|
||||
modifier = Modifier.clickable { viewModel.setViewMode(mode) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 热力图 Canvas ──
|
||||
HeatmapCanvas(
|
||||
state = state,
|
||||
onDayClick = { dayData ->
|
||||
// Phase 4: 显示浮层
|
||||
},
|
||||
onWeekClick = { weekData ->
|
||||
// Phase 4: 显示浮层
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -492,4 +492,15 @@
|
||||
<string name="action_submit_callback">提交回调 URL</string>
|
||||
<string name="action_regenerate_link">重新生成授权链接</string>
|
||||
<string name="oauth_preparing">正在准备授权...</string>
|
||||
|
||||
<!-- ══════════ Token 活动热力图 ══════════ -->
|
||||
<string name="heatmap_title">Token 活动</string>
|
||||
<string name="heatmap_view_daily">每日</string>
|
||||
<string name="heatmap_view_weekly">每周</string>
|
||||
<string name="heatmap_view_cumulative">累计</string>
|
||||
<string name="heatmap_entry">Token 活动</string>
|
||||
<string name="heatmap_entry_subtitle">每日 Token 使用热力图</string>
|
||||
<string name="heatmap_no_data">暂无数据</string>
|
||||
<string name="heatmap_legend_less">少</string>
|
||||
<string name="heatmap_legend_more">多</string>
|
||||
</resources>
|
||||
@ -492,4 +492,15 @@
|
||||
<string name="action_submit_callback">提交回撥 URL</string>
|
||||
<string name="action_regenerate_link">重新生成授權連結</string>
|
||||
<string name="oauth_preparing">正在準備授權...</string>
|
||||
|
||||
<!-- ══════════ Token 活动热力图 ══════════ -->
|
||||
<string name="heatmap_title">Token 活動</string>
|
||||
<string name="heatmap_view_daily">每日</string>
|
||||
<string name="heatmap_view_weekly">每週</string>
|
||||
<string name="heatmap_view_cumulative">累計</string>
|
||||
<string name="heatmap_entry">Token 活動</string>
|
||||
<string name="heatmap_entry_subtitle">每日 Token 使用熱力圖</string>
|
||||
<string name="heatmap_no_data">暫無資料</string>
|
||||
<string name="heatmap_legend_less">少</string>
|
||||
<string name="heatmap_legend_more">多</string>
|
||||
</resources>
|
||||
@ -495,4 +495,15 @@
|
||||
<string name="action_submit_callback">Submit callback URL</string>
|
||||
<string name="action_regenerate_link">Regenerate auth link</string>
|
||||
<string name="oauth_preparing">Preparing authorization...</string>
|
||||
|
||||
<!-- ══════════ Token 活动热力图 ══════════ -->
|
||||
<string name="heatmap_title">Token Activity</string>
|
||||
<string name="heatmap_view_daily">Daily</string>
|
||||
<string name="heatmap_view_weekly">Weekly</string>
|
||||
<string name="heatmap_view_cumulative">Cumulative</string>
|
||||
<string name="heatmap_entry">Token Activity</string>
|
||||
<string name="heatmap_entry_subtitle">Daily Token usage heatmap</string>
|
||||
<string name="heatmap_no_data">No data yet</string>
|
||||
<string name="heatmap_legend_less">Less</string>
|
||||
<string name="heatmap_legend_more">More</string>
|
||||
</resources>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user