feat(sub2api): 重做首页卡片布局——余额主视觉 + 今日/近7天等宽磁贴

原布局把余额与周期数据塞在同一行,窄屏(360dp)会换行、数字长短不一还容易错位,
整体观感松散。改为上下分区:

- 余额区:「余额」小标签 + 大号数字 + 单位,单行左对齐,作为卡片主视觉。
- 用量区:「今日 / 近7天」两块等宽磁贴(14dp 圆角 + 半透明底色),
  每块固定三行(周期名 / Token 数 / 请求次数),内容垂直居中,
  两块结构与内边距一致,数值长短不同也不会错位。
- 卡片正面只保留 余额、Token 用量、请求次数三项;不再显示花费与「本月已用」
  (花费与更长周期明细仍可在详情页时间窗口查看)。

副标题改为登录方式:
- Sub2API 卡片副标题由「自托管面板 · 用户级 sk- Key 余额」改为「邮箱登录 / API 登录」,
  依据 Repository 新写入的 extras["auth_mode"](panel / apikey)判定,无数据时回退原描述。

真机验证(V2172A / Android 15,360dp 宽):
余额 y887-1105 与两块磁贴 y1185-1405 分区清晰、不换行;磁贴在 x176 与 x788 等宽对齐;
副标题显示「邮箱登录」。compileDebugKotlin 与 lintDebug 均通过。
This commit is contained in:
Liuxinyu176 2026-09-12 10:14:15 +08:00
parent ce8a656448
commit 56f85d296c
6 changed files with 155 additions and 53 deletions

View File

@ -320,6 +320,8 @@ class Sub2ApiRepository constructor(
val extras = linkedMapOf<String, String>()
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<String, String>()
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()

View File

@ -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()
)

View File

@ -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<String> {
private fun buildSub2SummaryWindows(detail: Sub2UsageDetail?): List<Sub2WindowSummary> {
val days = detail?.dailyUsage.orEmpty()
if (days.isEmpty()) return emptyList()
val today = LocalDate.now()
@ -683,21 +760,26 @@ private fun buildSub2SummaryRows(detail: Sub2UsageDetail?): List<String> {
}
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)

View File

@ -673,4 +673,8 @@
<string name="sub2_panel_low_balance">⚠ 余额不足:剩余 $%1$s,请及时充值或兑换卡密</string>
<string name="sub2_models_alltime_suffix">(累计口径,未按所选周期过滤)</string>
<string name="sub2_panel_keys_header">API Key 列表</string>
<string name="sub2_auth_panel">邮箱登录</string>
<string name="sub2_auth_apikey">API 登录</string>
<string name="sub2_home_requests">%1$d 次</string>
<string name="sub2_balance_label">余额</string>
</resources>

View File

@ -673,4 +673,8 @@
<string name="sub2_panel_low_balance">⚠ 餘額不足:剩餘 $%1$s,請及時儲值或兌換卡密</string>
<string name="sub2_models_alltime_suffix">(累計口徑,未按所選週期過濾)</string>
<string name="sub2_panel_keys_header">API Key 列表</string>
<string name="sub2_auth_panel">信箱登錄</string>
<string name="sub2_auth_apikey">API 登錄</string>
<string name="sub2_home_requests">%1$d 次</string>
<string name="sub2_balance_label">餘額</string>
</resources>

View File

@ -678,4 +678,8 @@
<string name="sub2_panel_low_balance">⚠ Low balance: $%1$s left — top up or redeem a code</string>
<string name="sub2_models_alltime_suffix">(all-time, not limited to the selected period)</string>
<string name="sub2_panel_keys_header">API keys</string>
<string name="sub2_auth_panel">Email login</string>
<string name="sub2_auth_apikey">API key login</string>
<string name="sub2_home_requests">%1$d req</string>
<string name="sub2_balance_label">Balance</string>
</resources>