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

View File

@ -117,10 +117,8 @@ fun ServiceDetailScreen(
val selectedModel by viewModel.selectedModel.collectAsStateWithLifecycle() val selectedModel by viewModel.selectedModel.collectAsStateWithLifecycle()
val modelsLoading by viewModel.modelsLoading.collectAsStateWithLifecycle() val modelsLoading by viewModel.modelsLoading.collectAsStateWithLifecycle()
val checkinState by viewModel.checkinState.collectAsStateWithLifecycle() val checkinState by viewModel.checkinState.collectAsStateWithLifecycle()
val panelKeysState by viewModel.panelKeysState.collectAsStateWithLifecycle() // 面板管理(Key/分组/兑换)已移到首页 Sub2API 卡片的「面板管理」入口,
val panelActionMessage by viewModel.panelActionMessage.collectAsStateWithLifecycle() // 详情页只保留用量明细所需的窗口统计状态。
val panelGroups by viewModel.panelGroups.collectAsStateWithLifecycle()
val recentlyCreatedKey by viewModel.recentlyCreatedKey.collectAsStateWithLifecycle()
val panelWindowStats by viewModel.panelWindowStats.collectAsStateWithLifecycle() val panelWindowStats by viewModel.panelWindowStats.collectAsStateWithLifecycle()
val config = ServiceConfigProvider.get(service) val config = ServiceConfigProvider.get(service)
val isManualMode = config.method == FetchMethod.MANUAL val isManualMode = config.method == FetchMethod.MANUAL
@ -210,24 +208,8 @@ fun ServiceDetailScreen(
onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) } onLoadWindowModelStats = { from, to -> viewModel.loadPanelWindowModelStats(from, to) }
) )
} }
// 面板管理:Key 列表/新建/分组/兑换/低余额提醒(有面板 Token 才加载) // 面板管理(Key 列表/新建/分组/兑换)不再在详情页展示:
item { // 入口统一放在首页 Sub2API 卡片的「面板管理」按钮,避免同页重复且详情页过长。
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
)
}
} }
} }

View File

@ -75,7 +75,9 @@ internal fun Sub2ApiPanelManagement(
onSwitchGroup: (Long, Long) -> Unit, onSwitchGroup: (Long, Long) -> Unit,
onRedeem: (String) -> Unit, onRedeem: (String) -> Unit,
onConsumeMessage: () -> Unit, onConsumeMessage: () -> Unit,
onConsumeCreatedKey: () -> Unit = {} onConsumeCreatedKey: () -> Unit = {},
/** 弹窗场景由 AlertDialog 的 title 槽展示标题,可关闭内部标题避免重复 */
showHeader: Boolean = true
) { ) {
val context = LocalContext.current val context = LocalContext.current
var showCreate by remember { mutableStateOf(false) } var showCreate by remember { mutableStateOf(false) }
@ -110,6 +112,7 @@ internal fun Sub2ApiPanelManagement(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
) { ) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
if (showHeader) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = stringResource(R.string.sub2_panel_manage_title), 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)) 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 -> (actionMessage ?: lastResult)?.let { msg ->
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -154,6 +166,26 @@ internal fun Sub2ApiPanelManagement(
Spacer(modifier = Modifier.height(12.dp)) 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) { when (keysState) {
is PanelKeysState.Idle -> { is PanelKeysState.Idle -> {
OutlinedButton(onClick = onLoadKeys, modifier = Modifier.fillMaxWidth()) { 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_copied">API Key 已复制</string>
<string name="sub2_panel_low_balance">⚠ 余额不足:剩余 $%1$s,请及时充值或兑换卡密</string> <string name="sub2_panel_low_balance">⚠ 余额不足:剩余 $%1$s,请及时充值或兑换卡密</string>
<string name="sub2_models_alltime_suffix">(累计口径,未按所选周期过滤)</string> <string name="sub2_models_alltime_suffix">(累计口径,未按所选周期过滤)</string>
<string name="sub2_panel_keys_header">API Key 列表</string>
</resources> </resources>

View File

@ -672,4 +672,5 @@
<string name="sub2_panel_copied">API Key 已複製</string> <string name="sub2_panel_copied">API Key 已複製</string>
<string name="sub2_panel_low_balance">⚠ 餘額不足:剩餘 $%1$s,請及時儲值或兌換卡密</string> <string name="sub2_panel_low_balance">⚠ 餘額不足:剩餘 $%1$s,請及時儲值或兌換卡密</string>
<string name="sub2_models_alltime_suffix">(累計口徑,未按所選週期過濾)</string> <string name="sub2_models_alltime_suffix">(累計口徑,未按所選週期過濾)</string>
<string name="sub2_panel_keys_header">API Key 列表</string>
</resources> </resources>

View File

@ -677,4 +677,5 @@
<string name="sub2_panel_copied">API Key copied</string> <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_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_models_alltime_suffix">(all-time, not limited to the selected period)</string>
<string name="sub2_panel_keys_header">API keys</string>
</resources> </resources>