fix(sub2api): 面板管理只保留首页入口,弹窗支持滚动、操作前置

按使用反馈调整(真机验证):
- 详情页不再重复展示面板管理卡:Key 列表/新建/分组/兑换统一收到首页 Sub2API 卡片的
  「面板管理」入口,详情页只保留用量明细(周期切换 + 柱状图 + 按模型)。
  随之移除详情页已不再使用的 panelKeysState / panelGroups / panelActionMessage 状态收集。
- 修复「面板管理弹窗打开后不能滚动」:AlertDialog 的 text 槽是固定高度且不自带滚动容器,
  内容超过上限会被直接裁剪。改为给 text 槽加 verticalScroll + 高度上限(屏幕高度 72%),
  现在可以正常上下滚动浏览全部 Key。
- 主要操作前置:新建 Key / 兑换卡密 从列表底部移到顶部(原位置在 20+ 条 Key 之后,
  需要滚到底才能看到);并新增「API Key 列表」小标题区分列表区。
- 弹窗内不再重复标题:组件新增 showHeader 参数,首页弹窗由 AlertDialog title 槽显示标题,
  组件内部标题隐藏(刷新按钮保留)。

验证:compileDebugKotlin、lintDebug 通过;真机(V2172A / Android 15)实测
首页仍只显示今日与近7天、弹窗可滚动、新建/兑换在首屏、详情页无面板管理卡。
注:gradle.properties 的本地路径开关仍未纳入提交。
This commit is contained in:
Liuxinyu176 2026-09-12 09:40:59 +08:00
parent 58d8b0e077
commit eebe9f6987
6 changed files with 79 additions and 57 deletions

View File

@ -19,12 +19,14 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@ -373,12 +375,23 @@ fun DashboardScreen(
}
}
// Sub2API 面板管理弹窗:与详情页共用同一个面板管理组件
// Sub2API 面板管理弹窗
// 滚动说明:AlertDialog 的 text 槽是固定高度且**不自带滚动容器**,内容超过上限会被直接裁掉
// (表现为「打开后滚不动、下面的 Key 看不到」)。这里给 text 槽加 verticalScroll + 高度上限,
// 让整个面板管理内容(Key 列表/新建/分组/兑换)可以正常上下滚动。
if (showSub2PanelManagement) {
val scrollState = rememberScrollState()
val maxDialogHeight = (LocalConfiguration.current.screenHeightDp * 0.72f).dp
AlertDialog(
onDismissRequest = { showSub2PanelManagement = false },
title = { Text(stringResource(R.string.sub2_panel_manage_title), fontWeight = FontWeight.Bold) },
text = {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = maxDialogHeight)
.verticalScroll(scrollState)
) {
com.rainy.token.ui.servicedetail.Sub2ApiPanelManagement(
balance = uiState.cards.firstOrNull { it.service == ServiceType.SUB2API }?.cachedBalance?.balance,
keysState = panelKeysState,
@ -391,8 +404,10 @@ fun DashboardScreen(
onSwitchGroup = viewModel::switchPanelGroup,
onRedeem = viewModel::redeemPanelCode,
onConsumeMessage = viewModel::consumePanelActionMessage,
onConsumeCreatedKey = viewModel::consumeRecentlyCreatedKey
onConsumeCreatedKey = viewModel::consumeRecentlyCreatedKey,
showHeader = false
)
}
},
confirmButton = {
TextButton(onClick = { showSub2PanelManagement = false }) {

View File

@ -117,10 +117,8 @@ fun ServiceDetailScreen(
val selectedModel by viewModel.selectedModel.collectAsStateWithLifecycle()
val modelsLoading by viewModel.modelsLoading.collectAsStateWithLifecycle()
val checkinState by viewModel.checkinState.collectAsStateWithLifecycle()
val panelKeysState by viewModel.panelKeysState.collectAsStateWithLifecycle()
val panelActionMessage by viewModel.panelActionMessage.collectAsStateWithLifecycle()
val panelGroups by viewModel.panelGroups.collectAsStateWithLifecycle()
val recentlyCreatedKey by viewModel.recentlyCreatedKey.collectAsStateWithLifecycle()
// 面板管理(Key/分组/兑换)已移到首页 Sub2API 卡片的「面板管理」入口,
// 详情页只保留用量明细所需的窗口统计状态。
val panelWindowStats by viewModel.panelWindowStats.collectAsStateWithLifecycle()
val config = ServiceConfigProvider.get(service)
val isManualMode = config.method == FetchMethod.MANUAL
@ -210,24 +208,8 @@ fun ServiceDetailScreen(
onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) }
)
}
// 面板管理:Key 列表/新建/分组/兑换/低余额提醒(有面板 Token 才加载)
item {
Sub2ApiPanelManagement(
balance = (uiState.state as? State.Fresh)?.data
?: (uiState.state as? State.Stale)?.data,
keysState = panelKeysState,
groups = panelGroups,
actionMessage = panelActionMessage,
createdKey = recentlyCreatedKey,
onLoadKeys = { viewModel.loadPanelKeys() },
onLoadGroups = { viewModel.loadPanelGroups() },
onCreateKey = viewModel::createPanelKey,
onSwitchGroup = viewModel::switchPanelGroup,
onRedeem = viewModel::redeemPanelCode,
onConsumeMessage = viewModel::consumePanelActionMessage,
onConsumeCreatedKey = viewModel::consumeRecentlyCreatedKey
)
}
// 面板管理(Key 列表/新建/分组/兑换)不再在详情页展示:
// 入口统一放在首页 Sub2API 卡片的「面板管理」按钮,避免同页重复且详情页过长。
}
}

View File

@ -75,7 +75,9 @@ internal fun Sub2ApiPanelManagement(
onSwitchGroup: (Long, Long) -> Unit,
onRedeem: (String) -> Unit,
onConsumeMessage: () -> Unit,
onConsumeCreatedKey: () -> Unit = {}
onConsumeCreatedKey: () -> Unit = {},
/** 弹窗场景由 AlertDialog 的 title 槽展示标题,可关闭内部标题避免重复 */
showHeader: Boolean = true
) {
val context = LocalContext.current
var showCreate by remember { mutableStateOf(false) }
@ -110,6 +112,7 @@ internal fun Sub2ApiPanelManagement(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
if (showHeader) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.sub2_panel_manage_title),
@ -121,6 +124,15 @@ internal fun Sub2ApiPanelManagement(
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.action_refresh))
}
}
} else {
// 标题由外部弹窗提供时,这里保留一个刷新按钮,方便手动重新拉取 Key/分组
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = { onLoadKeys(); onLoadGroups() }) {
Icon(Icons.Filled.Refresh, contentDescription = stringResource(R.string.action_refresh))
}
}
}
(actionMessage ?: lastResult)?.let { msg ->
Spacer(modifier = Modifier.height(8.dp))
@ -154,6 +166,26 @@ internal fun Sub2ApiPanelManagement(
Spacer(modifier = Modifier.height(12.dp))
// 主要操作(新建 Key / 兑换卡密)固定在列表上方:
// Key 数量多时列表很长,放在底部会被埋在滚动区尽头,不易发现。
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { onLoadGroups(); showCreate = true }, modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.sub2_panel_create))
}
OutlinedButton(onClick = { showRedeem = true }, modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.sub2_panel_redeem))
}
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.sub2_panel_keys_header),
style = MaterialTheme.typography.labelLarge,
color = inkMuted()
)
Spacer(modifier = Modifier.height(4.dp))
when (keysState) {
is PanelKeysState.Idle -> {
OutlinedButton(onClick = onLoadKeys, modifier = Modifier.fillMaxWidth()) {
@ -225,16 +257,6 @@ internal fun Sub2ApiPanelManagement(
}
}
Spacer(modifier = Modifier.height(10.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { onLoadGroups(); showCreate = true }, modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.sub2_panel_create))
}
OutlinedButton(onClick = { showRedeem = true }, modifier = Modifier.weight(1f)) {
Text(stringResource(R.string.sub2_panel_redeem))
}
}
}
}

View File

@ -672,4 +672,5 @@
<string name="sub2_panel_copied">API Key 已复制</string>
<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>
</resources>

View File

@ -672,4 +672,5 @@
<string name="sub2_panel_copied">API Key 已複製</string>
<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>
</resources>

View File

@ -677,4 +677,5 @@
<string name="sub2_panel_copied">API Key copied</string>
<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>
</resources>