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:
parent
ce8a656448
commit
56f85d296c
@ -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()
|
||||
|
||||
@ -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()
|
||||
)
|
||||
|
||||
@ -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) {
|
||||
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 = formatAmount(balance.amount),
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
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 rows = remember(detail) { buildSub2SummaryRows(detail) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
if (rows.isEmpty()) {
|
||||
val windows = remember(detail) { buildSub2SummaryWindows(detail) }
|
||||
if (windows.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.sub2_home_no_usage),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = inkMuted()
|
||||
)
|
||||
} else {
|
||||
rows.forEach { row ->
|
||||
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)
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user