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.UsageDetailScreen
|
||||||
import com.rainy.token.ui.dashboard.UsageOverviewScreen
|
import com.rainy.token.ui.dashboard.UsageOverviewScreen
|
||||||
import com.rainy.token.ui.dashboard.UsageViewModel
|
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.servicedetail.ServiceDetailScreen
|
||||||
import com.rainy.token.ui.settings.CredentialEditScreen
|
import com.rainy.token.ui.settings.CredentialEditScreen
|
||||||
import com.rainy.token.ui.settings.SettingsScreen
|
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_DETAIL = "ccgo_usage_detail"
|
||||||
const val CCGO_USAGE_OVERVIEW = "ccgo_usage_overview"
|
const val CCGO_USAGE_OVERVIEW = "ccgo_usage_overview"
|
||||||
const val CCGO_USAGE_DATA = "ccgo_usage_data"
|
const val CCGO_USAGE_DATA = "ccgo_usage_data"
|
||||||
|
const val HEATMAP = "heatmap"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseServiceType(typeName: String?): ServiceType =
|
private fun parseServiceType(typeName: String?): ServiceType =
|
||||||
@ -96,6 +98,7 @@ private sealed class DetailPane {
|
|||||||
object OCGOUsage : DetailPane()
|
object OCGOUsage : DetailPane()
|
||||||
object CCGOUsage : DetailPane()
|
object CCGOUsage : DetailPane()
|
||||||
object Settings : DetailPane()
|
object Settings : DetailPane()
|
||||||
|
object Heatmap : DetailPane()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -191,7 +194,8 @@ private fun CompactNavHost() {
|
|||||||
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
||||||
onOpenService = { type -> navController.navigate(Routes.serviceDetail(type)) },
|
onOpenService = { type -> navController.navigate(Routes.serviceDetail(type)) },
|
||||||
onOpenUsageDetail = { navController.navigate(Routes.USAGE_DETAIL) },
|
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) {
|
composable(Routes.USAGE_DETAIL) {
|
||||||
@ -303,6 +307,9 @@ private fun CompactNavHost() {
|
|||||||
onStartWebViewLogin = { svc -> navController.navigate(Routes.webviewLogin(svc)) }
|
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) },
|
onOpenService = { type -> detailPane = DetailPane.ServiceDetail(type) },
|
||||||
onOpenUsageDetail = { detailPane = DetailPane.OCGOUsage },
|
onOpenUsageDetail = { detailPane = DetailPane.OCGOUsage },
|
||||||
onOpenCcgoUsageDetail = { detailPane = DetailPane.CCGOUsage },
|
onOpenCcgoUsageDetail = { detailPane = DetailPane.CCGOUsage },
|
||||||
|
onOpenHeatmap = { detailPane = DetailPane.Heatmap },
|
||||||
viewModel = dashboardVm
|
viewModel = dashboardVm
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -442,6 +450,9 @@ private fun ExpandedDetailPane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is DetailPane.Heatmap -> {
|
||||||
|
HeatmapScreen(onBack = onClose)
|
||||||
|
}
|
||||||
is DetailPane.Settings -> {
|
is DetailPane.Settings -> {
|
||||||
val settingsNavController = rememberNavController()
|
val settingsNavController = rememberNavController()
|
||||||
// 监听嵌套 NavHost 路由变化:当从凭据编辑页/WebView 返回 settings_main 时
|
// 监听嵌套 NavHost 路由变化:当从凭据编辑页/WebView 返回 settings_main 时
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
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.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
@ -122,6 +123,7 @@ fun DashboardScreen(
|
|||||||
onOpenService: (ServiceType) -> Unit,
|
onOpenService: (ServiceType) -> Unit,
|
||||||
onOpenUsageDetail: () -> Unit,
|
onOpenUsageDetail: () -> Unit,
|
||||||
onOpenCcgoUsageDetail: () -> Unit = {},
|
onOpenCcgoUsageDetail: () -> Unit = {},
|
||||||
|
onOpenHeatmap: () -> Unit = {},
|
||||||
viewModel: DashboardViewModel = hiltViewModel()
|
viewModel: DashboardViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
// 从设置页返回时重新读取本地凭据状态 + 缓存(不自动发起网络请求)
|
// 从设置页返回时重新读取本地凭据状态 + 缓存(不自动发起网络请求)
|
||||||
@ -280,6 +282,7 @@ fun DashboardScreen(
|
|||||||
onOpenUsageDetail = onOpenUsageDetail,
|
onOpenUsageDetail = onOpenUsageDetail,
|
||||||
onOpenCcgoUsageDetail = onOpenCcgoUsageDetail,
|
onOpenCcgoUsageDetail = onOpenCcgoUsageDetail,
|
||||||
onOpenService = onOpenService,
|
onOpenService = onOpenService,
|
||||||
|
onOpenHeatmap = onOpenHeatmap,
|
||||||
refreshTrigger = usageSyncTrigger
|
refreshTrigger = usageSyncTrigger
|
||||||
)
|
)
|
||||||
DraggableDashboardCards(
|
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_OCGO_CARD_ID = "usage:opencode_go"
|
||||||
private const val USAGE_CCGO_CARD_ID = "usage:commandcode_go"
|
private const val USAGE_CCGO_CARD_ID = "usage:commandcode_go"
|
||||||
private const val DASHBOARD_CARD_SPACING_DP = 12
|
private const val DASHBOARD_CARD_SPACING_DP = 12
|
||||||
|
private const val HEATMAP_CARD_ID = "heatmap"
|
||||||
|
|
||||||
private data class DashboardHomeItem(
|
private data class DashboardHomeItem(
|
||||||
val id: String,
|
val id: String,
|
||||||
@ -359,6 +363,7 @@ private fun rememberDashboardItems(
|
|||||||
onOpenUsageDetail: () -> Unit,
|
onOpenUsageDetail: () -> Unit,
|
||||||
onOpenCcgoUsageDetail: () -> Unit,
|
onOpenCcgoUsageDetail: () -> Unit,
|
||||||
onOpenService: (ServiceType) -> Unit,
|
onOpenService: (ServiceType) -> Unit,
|
||||||
|
onOpenHeatmap: () -> Unit,
|
||||||
refreshTrigger: Int
|
refreshTrigger: Int
|
||||||
): List<DashboardHomeItem> {
|
): List<DashboardHomeItem> {
|
||||||
val defaultItems = buildList {
|
val defaultItems = buildList {
|
||||||
@ -368,6 +373,9 @@ private fun rememberDashboardItems(
|
|||||||
add(DashboardHomeItem(USAGE_CCGO_CARD_ID) {
|
add(DashboardHomeItem(USAGE_CCGO_CARD_ID) {
|
||||||
CommandCodeUsageStatsCard(onOpenDetail = onOpenCcgoUsageDetail, refreshTrigger = refreshTrigger)
|
CommandCodeUsageStatsCard(onOpenDetail = onOpenCcgoUsageDetail, refreshTrigger = refreshTrigger)
|
||||||
})
|
})
|
||||||
|
add(DashboardHomeItem(HEATMAP_CARD_ID) {
|
||||||
|
HeatmapEntryCard(onOpenHeatmap = onOpenHeatmap)
|
||||||
|
})
|
||||||
cards.forEach { card ->
|
cards.forEach { card ->
|
||||||
add(DashboardHomeItem("service:${card.service.storageKey}") {
|
add(DashboardHomeItem("service:${card.service.storageKey}") {
|
||||||
DashboardCard(
|
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:填空白 + 显示版本号。
|
* 仪表盘底部 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_submit_callback">提交回调 URL</string>
|
||||||
<string name="action_regenerate_link">重新生成授权链接</string>
|
<string name="action_regenerate_link">重新生成授权链接</string>
|
||||||
<string name="oauth_preparing">正在准备授权...</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>
|
</resources>
|
||||||
@ -492,4 +492,15 @@
|
|||||||
<string name="action_submit_callback">提交回撥 URL</string>
|
<string name="action_submit_callback">提交回撥 URL</string>
|
||||||
<string name="action_regenerate_link">重新生成授權連結</string>
|
<string name="action_regenerate_link">重新生成授權連結</string>
|
||||||
<string name="oauth_preparing">正在準備授權...</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>
|
</resources>
|
||||||
@ -495,4 +495,15 @@
|
|||||||
<string name="action_submit_callback">Submit callback URL</string>
|
<string name="action_submit_callback">Submit callback URL</string>
|
||||||
<string name="action_regenerate_link">Regenerate auth link</string>
|
<string name="action_regenerate_link">Regenerate auth link</string>
|
||||||
<string name="oauth_preparing">Preparing authorization...</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>
|
</resources>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user