diff --git a/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt b/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt index 86bf0b3..5bfb597 100644 --- a/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt +++ b/app/src/main/java/com/rainy/token/data/repository/Sub2ApiRepository.kt @@ -320,6 +320,8 @@ class Sub2ApiRepository constructor( val extras = linkedMapOf() extras["scope"] = "panel account (all keys)" + // 认证方式:首页卡片用它显示「邮箱登录 / API 登录」这类简短标签(而不是笼统的「自托管面板…」) + extras["auth_mode"] = "panel" extras["month_start"] = monthStart if (totalRequests > 0) extras["total_requests"] = totalRequests.toString() if (totalTokens > 0) extras["total_tokens"] = totalTokens.toString() @@ -836,6 +838,7 @@ class Sub2ApiRepository constructor( val valid = explicitValid ?: hasData val extras = linkedMapOf() + extras["auth_mode"] = "apikey" top.scalar("planName", "plan_name", "plan")?.let { extras["plan"] = it } top.scalar("mode")?.let { extras["mode"] = it } extras["valid"] = valid.toString() diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt b/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt index 14351d9..cbe559e 100644 --- a/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/rainy/token/ui/dashboard/DashboardScreen.kt @@ -767,7 +767,12 @@ private fun DashboardCard( fontWeight = FontWeight.SemiBold ) Text( - text = stringResource(secondaryLineRes(card)), + // Sub2API 显示登录方式(邮箱登录 / API 登录),其余服务用固定描述 + text = if (card.service == ServiceType.SUB2API) { + sub2AuthModeText(card) + } else { + stringResource(secondaryLineRes(card)) + }, style = MaterialTheme.typography.bodySmall, color = inkMuted() ) diff --git a/app/src/main/java/com/rainy/token/ui/dashboard/ServiceBalanceCards.kt b/app/src/main/java/com/rainy/token/ui/dashboard/ServiceBalanceCards.kt index 9bdfad3..6c2998a 100644 --- a/app/src/main/java/com/rainy/token/ui/dashboard/ServiceBalanceCards.kt +++ b/app/src/main/java/com/rainy/token/ui/dashboard/ServiceBalanceCards.kt @@ -1,6 +1,7 @@ package com.rainy.token.ui.dashboard import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.background import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme @@ -19,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.res.stringResource import androidx.annotation.StringRes @@ -96,9 +99,9 @@ internal fun BalanceMainArea(card: DashboardCardUi) { OllamaUsageWindows(balance) } card.service == ServiceType.SUB2API -> { - Sub2ApiMainBalance(balance) - Spacer(modifier = Modifier.height(12.dp)) - Sub2ApiUsageSummary(balance) + // 余额与「今日 / 近7天」并排:右侧两列紧凑展示,避免早先竖排时把卡片撑得过高、 + // 也避免在窄屏上把周期行挤到换行。 + Sub2ApiBalanceWithUsage(balance) } else -> { Row(verticalAlignment = Alignment.Bottom) { @@ -556,6 +559,24 @@ internal fun DashboardCardUi.statusBadgeStyle(): StatusStyle = when { StatusStyle(R.string.status_normal, StatusLevel.OK) } +/** + * Sub2API 副标题:显示登录方式(邮箱登录 / API 登录),而不是笼统的「自托管面板 · 用户级 sk- Key 余额」。 + * 依据 UsageDetail/余额 extras 里的 auth_mode(由 Repository 写入): + * panel = 面板 Token(邮箱/账号登录面板) + * apikey = 仅配置了 sk- API Key + * 无数据时回退到通用描述。 + */ +@Composable +internal fun sub2AuthModeText(card: DashboardCardUi): String { + val balance = card.displayBalance + val mode = balance?.extras?.get("auth_mode") + return when (mode) { + "panel" -> stringResource(R.string.sub2_auth_panel) + "apikey" -> stringResource(R.string.sub2_auth_apikey) + else -> stringResource(R.string.service_desc_sub2api) + } +} + /** 服务副标题(资源 ID,由 UI 层解析)。 */ @StringRes internal fun secondaryLineRes(card: DashboardCardUi): Int = when (card.service) { @@ -618,50 +639,106 @@ internal fun usageUpdatedAtText(updatedAt: Long): String { } // ── Sub2API 首页卡片:余额 + 日/周/月/本月 用量汇总 ── +/** + * 首页 Sub2API 卡主体。 + * + * 布局: + * ① 余额区——「余额」小标签 + 大号数字 + 单位(带品牌色圆点的左侧强调条) + * ② 用量区——「今日 / 近7天」两块等宽磁贴,每块显示 请求次数 与 Token 数 + * + * 磁贴等宽 + 两行固定结构,保证 360dp 窄屏不换行、不同周期数值长度不一会错位。 + */ @Composable -internal fun Sub2ApiMainBalance(balance: ServiceBalance) { - Row(verticalAlignment = Alignment.Bottom) { - Text( - text = formatAmount(balance.amount), - style = MaterialTheme.typography.displayMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = balance.unit, - style = MaterialTheme.typography.titleMedium, - color = inkMuted(), - modifier = Modifier.padding(bottom = 6.dp) - ) - } - balance.monthlySpent?.let { spent -> - Text( - text = stringResource(R.string.service_monthly_used, formatAmount(spent), balance.unit), - style = MaterialTheme.typography.bodySmall, - color = inkMuted() - ) +internal fun Sub2ApiBalanceWithUsage(balance: ServiceBalance) { + Column(modifier = Modifier.fillMaxWidth()) { + Sub2ApiBalanceHero(balance) + Spacer(modifier = Modifier.height(10.dp)) + Sub2ApiUsageTiles(balance) } } -/** 首页 Sub2API 用量汇总:今日 / 近7天 / 近30天 / 本月 的请求数、Token 数与花费。 */ +/** 余额区:小标签 + 大数字 + 单位。 */ @Composable -internal fun Sub2ApiUsageSummary(balance: ServiceBalance) { - val detail = balance.sub2UsageDetail - val rows = remember(detail) { buildSub2SummaryRows(detail) } - Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - if (rows.isEmpty()) { +internal fun Sub2ApiMainBalance(balance: ServiceBalance, modifier: Modifier = Modifier) { + Sub2ApiBalanceHero(balance, modifier) +} + +@Composable +private fun Sub2ApiBalanceHero(balance: ServiceBalance, modifier: Modifier = Modifier) { + Column(modifier = modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.sub2_balance_label), + style = MaterialTheme.typography.labelMedium, + color = inkMuted(), + maxLines = 1 + ) + Spacer(modifier = Modifier.height(2.dp)) + Row(verticalAlignment = Alignment.Bottom) { Text( - text = stringResource(R.string.sub2_home_no_usage), - style = MaterialTheme.typography.bodySmall, - color = inkMuted() + text = formatAmount(balance.amount), + style = MaterialTheme.typography.displaySmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) - } else { - rows.forEach { row -> + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = balance.unit, + style = MaterialTheme.typography.titleSmall, + color = inkMuted(), + modifier = Modifier.padding(bottom = 4.dp), + maxLines = 1 + ) + } + } +} + +/** 用量区:今日 / 近7天 两块等宽磁贴。 */ +@Composable +private fun Sub2ApiUsageTiles(balance: ServiceBalance) { + val detail = balance.sub2UsageDetail + val windows = remember(detail) { buildSub2SummaryWindows(detail) } + if (windows.isEmpty()) { + Text( + text = stringResource(R.string.sub2_home_no_usage), + style = MaterialTheme.typography.bodySmall, + color = inkMuted() + ) + return + } + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + windows.forEach { w -> + Column( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(14.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.Center + ) { Text( - text = row, - style = MaterialTheme.typography.bodySmall, - color = inkMuted() + text = w.label, + style = MaterialTheme.typography.labelSmall, + color = inkMuted(), + maxLines = 1 + ) + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = w.tokens, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = stringResource(R.string.sub2_home_requests, w.requests), + style = MaterialTheme.typography.labelSmall, + color = inkMuted(), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } @@ -669,7 +746,7 @@ internal fun Sub2ApiUsageSummary(balance: ServiceBalance) { } /** 聚合 dailyUsage 计算各窗口汇总行文案;无数据返回空列表。 */ -private fun buildSub2SummaryRows(detail: Sub2UsageDetail?): List { +private fun buildSub2SummaryWindows(detail: Sub2UsageDetail?): List { val days = detail?.dailyUsage.orEmpty() if (days.isEmpty()) return emptyList() val today = LocalDate.now() @@ -683,21 +760,26 @@ private fun buildSub2SummaryRows(detail: Sub2UsageDetail?): List { } return Triple(req, tok, cost) } - data class W(val label: String, val from: LocalDate) // 首页仅显示 日/周 两个周期(近30天/本月 明细在详情页时间窗口里查看) - val windows = listOf( - W("今日", today), - W("近7天", today.minusDays(6)) - ) - return windows.map { w -> - val (req, tok, cost) = sum(w.from) - val tokens = if (tok >= 1_000_000_000) (tok / 1_000_000_000.0).formatLocale("%.1fB") - else if (tok >= 1_000_000) (tok / 1_000_000.0).formatLocale("%.1fM") - else if (tok >= 1_000) (tok / 1_000.0).formatLocale("%.1fK") - else tok.toString() - "${w.label} · ${req} 次 · $tokens tokens · $${cost.formatLocale("%.2f")}" + val windows = listOf("今日" to today, "近7天" to today.minusDays(6)) + return windows.map { (label, from) -> + val (req, tok, cost) = sum(from) + val tokens = when { + tok >= 1_000_000_000 -> (tok / 1_000_000_000.0).formatLocale("%.1fB") + tok >= 1_000_000 -> (tok / 1_000_000.0).formatLocale("%.1fM") + tok >= 1_000 -> (tok / 1_000.0).formatLocale("%.1fK") + else -> tok.toString() + } + Sub2WindowSummary(label = label, requests = req, tokens = tokens) } } +/** 首页周期汇总(今日 / 近7天)。 */ +private data class Sub2WindowSummary( + val label: String, + val requests: Long, + val tokens: String +) + private fun Double.formatLocale(pattern: String): String = String.format(java.util.Locale.getDefault(), pattern, this) diff --git a/app/src/main/res/values-b+zh+Hans/strings.xml b/app/src/main/res/values-b+zh+Hans/strings.xml index a659cd8..948d642 100644 --- a/app/src/main/res/values-b+zh+Hans/strings.xml +++ b/app/src/main/res/values-b+zh+Hans/strings.xml @@ -673,4 +673,8 @@ ⚠ 余额不足:剩余 $%1$s,请及时充值或兑换卡密 (累计口径,未按所选周期过滤) API Key 列表 + 邮箱登录 + API 登录 + %1$d 次 + 余额 \ No newline at end of file diff --git a/app/src/main/res/values-b+zh+Hant/strings.xml b/app/src/main/res/values-b+zh+Hant/strings.xml index a7a200c..a19959b 100644 --- a/app/src/main/res/values-b+zh+Hant/strings.xml +++ b/app/src/main/res/values-b+zh+Hant/strings.xml @@ -673,4 +673,8 @@ ⚠ 餘額不足:剩餘 $%1$s,請及時儲值或兌換卡密 (累計口徑,未按所選週期過濾) API Key 列表 + 信箱登錄 + API 登錄 + %1$d 次 + 餘額 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index beb8339..32b09dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -678,4 +678,8 @@ ⚠ Low balance: $%1$s left — top up or redeem a code (all-time, not limited to the selected period) API keys + Email login + API key login + %1$d req + Balance \ No newline at end of file