From 464d0adb1999e4bbcfd825e44e313d4be5163155 Mon Sep 17 00:00:00 2001 From: AllenTom Date: Wed, 12 Nov 2025 18:10:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=9B=E5=BB=BA=E7=BE=A4?= =?UTF-8?q?=E8=81=8A=E7=9A=84=E8=B4=B9=E7=94=A8=E5=92=8C=E4=BA=BA=E6=95=B0?= =?UTF-8?q?=E4=B8=8A=E9=99=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **创建群聊费用:** - 创建群聊现在会根据后台配置的积分规则扣除相应费用(派币)。 - 在创建页面会显示当前余额和所需费用。 - 创建时会弹出确认弹窗,显示费用、当前余额和扣除后余额。 - 如果余额不足,将无法创建。 - **群聊人数上限:** - 新增创建群聊时的初始成员人数上限,该上限从后台动态获取。 - 在选择成员界面会显示当前已选人数和上限(例如 `5/10`)。 - 如果选择的成员超过上限,会提示错误并且无法创建。 - **后台数据加载:** - 新增了从外部字典表 (`/outside/dict`) 获取配置的接口和逻辑,用于加载积分规则和群聊人数限制。 - App启动时会预加载这些配置,以确保创建群聊时能正确显示费用和人数限制。 --- .../ravenow/ui/agent/AiPromptEditScreen.kt | 260 ++++++------- .../ravenow/ui/agent/AiPromptEditViewModel.kt | 42 +++ .../ui/composables/PointsPaymentDialog.kt | 342 ++++++++++++++++++ .../ui/composables/form/FormTextInput.kt | 7 + .../ui/composables/form/FormTextInput2.kt | 7 + app/src/main/res/values-zh/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 7 files changed, 517 insertions(+), 151 deletions(-) create mode 100644 app/src/main/java/com/aiosman/ravenow/ui/composables/PointsPaymentDialog.kt diff --git a/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditScreen.kt index 2e94e49..c4c956e 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditScreen.kt @@ -33,10 +33,13 @@ import com.aiosman.ravenow.R import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.composables.ActionButton import com.aiosman.ravenow.ui.composables.CustomAsyncImage +import com.aiosman.ravenow.ui.composables.PointsPaymentDialog import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.form.FormTextInput import com.aiosman.ravenow.ui.composables.form.FormTextInput2 import com.aiosman.ravenow.ui.modifiers.noRippleClickable +import com.aiosman.ravenow.data.PointService +import androidx.compose.runtime.collectAsState import kotlinx.coroutines.launch /** @@ -71,23 +74,30 @@ fun AiPromptEditScreen( var errorMessage by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() + // 获取积分规则和余额 + val pointsRules by PointService.pointsRules.collectAsState(initial = null) + val pointsBalance by PointService.pointsBalance.collectAsState(initial = null) + // 计算是否需要付费 val needsPayment = viewModel.needsPrivacyPayment() - val privacyCost = 100 // 默认100钥匙,后续可以从PointService获取 + val privacyCost = viewModel.getPrivacyCost() + val currentBalance = viewModel.getCurrentBalance() + val balanceAfterCost = viewModel.calculateBalanceAfterCost(privacyCost) + val isBalanceSufficient = viewModel.isBalanceSufficient(privacyCost) Column( modifier = Modifier .fillMaxSize() - .background(color = appColors.background), + .background(color = Color(0xFFFAFAFB)), horizontalAlignment = Alignment.CenterHorizontally ) { StatusBarSpacer() - + // 顶部导航栏 Box( modifier = Modifier .fillMaxWidth() - .background(color = appColors.background) + .background(color = Color(0xFFFAFAFB)) .padding(horizontal = 14.dp, vertical = 16.dp) ) { Row( @@ -117,15 +127,15 @@ fun AiPromptEditScreen( ) } } - + Spacer(modifier = Modifier.height(1.dp)) - + // 内容区域 Column( modifier = Modifier .fillMaxWidth() .weight(1f) - .background(appColors.background) + .background(Color(0xFFFAFAFB)) ) { // 头像选择 Column( @@ -195,9 +205,9 @@ fun AiPromptEditScreen( } } } - + Spacer(modifier = Modifier.height(18.dp)) - + // 名称输入 Column( modifier = Modifier @@ -214,7 +224,7 @@ fun AiPromptEditScreen( FormTextInput( value = viewModel.title, hint = stringResource(R.string.agent_name_hint_1), - background = appColors.inputBackground2, + background = Color.White, modifier = Modifier.fillMaxWidth(), ) { value -> viewModel.title = value @@ -222,7 +232,7 @@ fun AiPromptEditScreen( } Spacer(modifier = Modifier.height(18.dp)) - + // 描述输入 Column( modifier = Modifier @@ -239,15 +249,15 @@ fun AiPromptEditScreen( FormTextInput2( value = viewModel.desc, hint = stringResource(R.string.agent_desc_hint), - background = appColors.inputBackground2, + background = Color.White, modifier = Modifier.fillMaxWidth(), ) { value -> viewModel.desc = value } } - + Spacer(modifier = Modifier.height(18.dp)) - + // 设定权限区域 Column( modifier = Modifier @@ -261,13 +271,18 @@ fun AiPromptEditScreen( fontWeight = FontWeight.W600 ) Spacer(modifier = Modifier.height(8.dp)) - + // 公开/私有切换 Row( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(appColors.inputBackground2) + .clip(RoundedCornerShape(25.dp)) + .background(Color.White) + .border( + width = 1.dp, + color = Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f), + shape = RoundedCornerShape(25.dp) + ) .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween @@ -298,58 +313,89 @@ fun AiPromptEditScreen( modifier = Modifier.size(width = 64.dp, height = 28.dp) ) } - + // 首次解锁AI权限提示 - if (needsPayment && !viewModel.paidForPrivacyEdit) { + if (needsPayment && !viewModel.paidForPrivacyEdit && privacyCost > 0) { Spacer(modifier = Modifier.height(8.dp)) + // 主要内容容器(去掉阴影) Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(appColors.inputBackground2.copy(alpha = 0.9f)) + .clip(RoundedCornerShape(16.dp)) + .background( + color = Color(red = 251f / 255f, green = 248f / 255f, blue = 239f / 255f) + ) + .border( + width = 1.dp, + color = Color(red = 243f / 255f, green = 234f / 255f, blue = 206f / 255f), + shape = RoundedCornerShape(16.dp) + ) .padding(12.dp) ) { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { + // 锁图标容器 + Box( + modifier = Modifier + .size(32.dp) + .background( + color = Color(red = 1f, green = 204f / 255f, blue = 0f, alpha = 0.12f), + shape = RoundedCornerShape(10.7.dp) + ), + contentAlignment = Alignment.Center + ) { + // 锁图标(使用文本代替,实际项目中可以使用图片资源) + Text( + text = "🔒", + fontSize = 18.sp + ) + } + Column( modifier = Modifier.weight(1f) ) { Text( - text = "首次解锁 AI 权限", + text = "首次解锁Ai权限", fontSize = 13.sp, - color = appColors.text, + color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f), fontWeight = FontWeight.W500 ) Spacer(modifier = Modifier.height(4.dp)) Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( text = "将消耗", fontSize = 12.sp, - color = appColors.text.copy(alpha = 0.6f) + color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f) ) - Spacer(modifier = Modifier.width(4.dp)) Text( text = "$privacyCost", fontSize = 12.sp, - color = appColors.brandColorsColor, - fontWeight = FontWeight.W500 + color = Color(red = 1f, green = 141f / 255f, blue = 40f / 255f) ) - Spacer(modifier = Modifier.width(2.dp)) - Text( - text = "钥匙", - fontSize = 12.sp, - color = appColors.brandColorsColor, - fontWeight = FontWeight.W500 + // 小硬币图标 + Box( + modifier = Modifier + .size(16.dp) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(0xFFFFD700), + Color(0xFFFFA500) + ) + ), + shape = CircleShape + ) ) - Spacer(modifier = Modifier.width(4.dp)) Text( text = "解锁后可随时切换", fontSize = 12.sp, - color = appColors.text.copy(alpha = 0.6f) + color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f) ) } } @@ -357,23 +403,13 @@ fun AiPromptEditScreen( } } } - - Spacer(modifier = Modifier.height(18.dp)) - - // 添加智能体记忆按钮 - AddAgentMemoryButton( - onAddMemoryClick = { - // TODO: 导航到记忆管理页面 - // navController.navigate(NavigationRoute.AgentMemoryManage.route.replace("{chatAIId}", chatAIId)) - } - ) } - + // 底部保存按钮 Box( modifier = Modifier .fillMaxWidth() - .background(color = appColors.background) + .background(color = Color(0xFFFAFAFB)) .padding(horizontal = 16.dp, vertical = 16.dp) ) { ActionButton( @@ -388,13 +424,13 @@ fun AiPromptEditScreen( errorMessage = validationError return@ActionButton } - + // 检查是否需要付费确认 if (needsPayment && !viewModel.paidForPrivacyEdit) { showPrivacyConfirmDialog = true return@ActionButton } - + // 执行保存 scope.launch { try { @@ -407,44 +443,35 @@ fun AiPromptEditScreen( } } } - + // 隐私权限付费确认对话框 if (showPrivacyConfirmDialog) { - // TODO: 实现付费确认对话框 - // 暂时直接切换,后续可以添加积分检查和扣减逻辑 - androidx.compose.material3.AlertDialog( - onDismissRequest = { showPrivacyConfirmDialog = false }, - title = { Text("升级隐私权限") }, - text = { Text("首次切换智能体的公开/私有状态需要支付一次性费用。支付后可自由在公有/私有之间切换,后续不再扣费。") }, - confirmButton = { - androidx.compose.material3.TextButton( - onClick = { - showPrivacyConfirmDialog = false - scope.launch { - try { - viewModel.isPublic = false - viewModel.updatePrompt(context) - viewModel.paidForPrivacyEdit = true - navController.navigateUp() - } catch (e: Exception) { - errorMessage = e.message ?: "保存失败" - } - } + PointsPaymentDialog( + cost = privacyCost, + currentBalance = currentBalance, + balanceAfterCost = balanceAfterCost, + isBalanceSufficient = isBalanceSufficient, + onConfirm = { + showPrivacyConfirmDialog = false + scope.launch { + try { + viewModel.isPublic = false + viewModel.updatePrompt(context) + viewModel.paidForPrivacyEdit = true + navController.navigateUp() + } catch (e: Exception) { + errorMessage = e.message ?: "保存失败" } - ) { - Text("确认支付") } }, - dismissButton = { - androidx.compose.material3.TextButton( - onClick = { showPrivacyConfirmDialog = false } - ) { - Text("取消") - } - } + onCancel = { + showPrivacyConfirmDialog = false + }, + title = "首次解锁AI权限", + description = "将消耗 $privacyCost 派币解锁后可随时切换" ) } - + // 错误提示 errorMessage?.let { error -> LaunchedEffect(error) { @@ -454,72 +481,3 @@ fun AiPromptEditScreen( // TODO: 显示Toast或Snackbar } } - -@Composable -private fun AddAgentMemoryButton( - onAddMemoryClick: () -> Unit -) { - val appColors = LocalAppTheme.current - - // 定义渐变边框颜色:紫色到蓝色 - val borderGradient = Brush.horizontalGradient( - colors = listOf( - Color(0xFF7C45ED), // 紫色 - Color(0xFF4A90E2) // 蓝色 - ) - ) - - // 浅紫色背景 - val lightPurpleBackground = Color(0xFFF5F0FF) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - // 使用两层Box来实现渐变边框效果 - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(brush = borderGradient) - .padding(1.5.dp) // 边框宽度 - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(lightPurpleBackground) - .padding(horizontal = 16.dp, vertical = 14.dp) - .noRippleClickable { - onAddMemoryClick() - }, - contentAlignment = Alignment.Center - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() - ) { - // 脑部图标 - Image( - painter = painterResource(id = R.drawable.ic_brain_add), - contentDescription = "添加智能体记忆", - modifier = Modifier.size(20.dp), - colorFilter = ColorFilter.tint(Color(0xFF7C45ED)) - ) - - Spacer(modifier = Modifier.width(8.dp)) - - // 文字 - Text( - text = "添加智能体记忆", - fontSize = 14.sp, - fontWeight = FontWeight.W600, - color = Color(0xFF7C45ED) - ) - } - } - } - } -} diff --git a/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditViewModel.kt index ba5821c..f3b73bb 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/agent/AiPromptEditViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aiosman.ravenow.data.UploadImage import com.aiosman.ravenow.data.ServiceException +import com.aiosman.ravenow.data.PointService import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.entity.AgentEntity import kotlinx.coroutines.launch @@ -145,6 +146,47 @@ class AiPromptEditViewModel : ViewModel() { return originalIsPublic == true && isPublic == false } + /** + * 获取解锁隐私权限的费用 + * @return 费用金额,如果无法获取则返回 0 + */ + fun getPrivacyCost(): Int { + val rules = PointService.pointsRules.value + val costRule = rules?.sub?.get(PointService.PointsRuleKey.SPEND_AGENT_PRIVATE) + return when (costRule) { + is PointService.RuleAmount.Fixed -> costRule.value + is PointService.RuleAmount.Range -> costRule.min // 使用最小值作为默认费用 + null -> 0 + } + } + + /** + * 获取当前余额 + * @return 当前余额,如果无法获取则返回 0 + */ + fun getCurrentBalance(): Int { + return PointService.pointsBalance.value?.balance ?: 0 + } + + /** + * 计算消耗后余额 + * @param cost 费用 + * @return 消耗后余额 + */ + fun calculateBalanceAfterCost(cost: Int): Int { + val currentBalance = getCurrentBalance() + return (currentBalance - cost).coerceAtLeast(0) + } + + /** + * 检查余额是否充足 + * @param cost 费用 + * @return 是否充足 + */ + fun isBalanceSufficient(cost: Int): Boolean { + return getCurrentBalance() >= cost + } + /** * 清空数据 */ diff --git a/app/src/main/java/com/aiosman/ravenow/ui/composables/PointsPaymentDialog.kt b/app/src/main/java/com/aiosman/ravenow/ui/composables/PointsPaymentDialog.kt new file mode 100644 index 0000000..9f79963 --- /dev/null +++ b/app/src/main/java/com/aiosman/ravenow/ui/composables/PointsPaymentDialog.kt @@ -0,0 +1,342 @@ +package com.aiosman.ravenow.ui.composables + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.aiosman.ravenow.LocalAppTheme +import com.aiosman.ravenow.R +import com.aiosman.ravenow.ui.modifiers.noRippleClickable + +/** + * 全局付费确认对话框组件 + * 参考 iOS 版本的 PointsConfirmDialog + * + * @param cost 需要支付的费用 + * @param currentBalance 当前余额 + * @param balanceAfterCost 支付后余额 + * @param isBalanceSufficient 余额是否充足 + * @param onConfirm 确认支付回调 + * @param onCancel 取消回调 + * @param title 对话框标题 + * @param description 对话框描述 + * @param isProcessing 是否正在处理中 + */ +@Composable +fun PointsPaymentDialog( + cost: Int, + currentBalance: Int, + balanceAfterCost: Int, + isBalanceSufficient: Boolean, + onConfirm: () -> Unit, + onCancel: () -> Unit, + title: String, + description: String, + isProcessing: Boolean = false +) { + val appColors = LocalAppTheme.current + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + val dialogWidth = (screenWidth - 48.dp).coerceAtMost(360.dp) + + Dialog( + onDismissRequest = { + if (!isProcessing) { + onCancel() + } + }, + properties = DialogProperties( + dismissOnBackPress = !isProcessing, + dismissOnClickOutside = !isProcessing + ) + ) { + Card( + modifier = Modifier + .width(dialogWidth) + .shadow( + elevation = 20.dp, + shape = RoundedCornerShape(20.dp), + spotColor = Color.Black.copy(alpha = 0.2f) + ), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = appColors.background + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 顶部图标 - 使用 paip_coin_img + Spacer(modifier = Modifier.height(32.dp)) + Image( + painter = painterResource(id = R.mipmap.paip_coin_img), + contentDescription = null, + modifier = Modifier.size(80.dp), + contentScale = ContentScale.Fit + ) + + // 标题 + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = title, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = appColors.text, + textAlign = TextAlign.Center + ) + + // 描述 + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = description, + fontSize = 14.sp, + color = appColors.secondaryText, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 20.dp) + ) + + // 积分消耗信息区域 + Spacer(modifier = Modifier.height(24.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = appColors.inputBackground.copy(alpha = 0.5f), + shape = RoundedCornerShape(12.dp) + ) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 需要消耗 + CostInfoRow( + label = stringResource(R.string.cost_required), + amount = cost, + appColors = appColors, + amountColor = Color(0xFFFF8C00) // 橙色 + ) + + HorizontalDivider(color = appColors.divider) + + // 当前余额 + CostInfoRow( + label = stringResource(R.string.current_balance), + amount = currentBalance, + appColors = appColors, + amountColor = if (isBalanceSufficient) appColors.text else Color.Red + ) + + HorizontalDivider(color = appColors.divider) + + // 支付后余额 + CostInfoRow( + label = stringResource(R.string.balance_after), + amount = balanceAfterCost, + appColors = appColors, + amountColor = appColors.text + ) + } + + // 余额不足提示 + if (!isBalanceSufficient) { + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = Color(0xFFFF8C00), // 橙色 + modifier = Modifier.size(16.dp) + ) + Text( + text = stringResource(R.string.insufficient_pai_coin_balance), + fontSize = 13.sp, + color = Color(0xFFFF8C00), // 橙色 + textAlign = TextAlign.Center + ) + } + } + + // 按钮 + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 取消按钮 + Button( + onClick = { + if (!isProcessing) { + onCancel() + } + }, + modifier = Modifier + .weight(1f) + .height(50.dp), + enabled = !isProcessing, + colors = ButtonDefaults.buttonColors( + containerColor = appColors.inputBackground, + contentColor = appColors.text, + disabledContainerColor = appColors.inputBackground, + disabledContentColor = appColors.text.copy(alpha = 0.5f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = stringResource(R.string.cancel), + fontSize = 16.sp, + fontWeight = FontWeight.W500 + ) + } + + // 确认按钮 + Box( + modifier = Modifier + .weight(1f) + .height(50.dp) + .background( + brush = if (isBalanceSufficient) { + Brush.horizontalGradient( + colors = listOf( + appColors.main, + appColors.main + ) + ) + } else { + Brush.horizontalGradient( + colors = listOf( + Color(0xFFFF8C00), // 橙色 + Color.Red + ) + ) + }, + shape = RoundedCornerShape(12.dp) + ) + .then( + if (!isProcessing) { + Modifier.noRippleClickable { + if (!isBalanceSufficient) { + // 积分不足,跳转充值页面 + onCancel() + // 这里可以发送通知或回调来跳转充值页面 + } else { + // 积分充足,确认消费 + onConfirm() + } + } + } else { + Modifier + } + ), + contentAlignment = Alignment.Center + ) { + if (isProcessing) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + text = if (isBalanceSufficient) { + stringResource(R.string.confirm_consumption) + } else { + stringResource(R.string.go_recharge) + }, + fontSize = 16.sp, + fontWeight = FontWeight.W600, + color = Color.White, + textAlign = TextAlign.Center + ) + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } + } +} + +/** + * 费用信息行组件 + */ +@Composable +private fun CostInfoRow( + label: String, + amount: Int, + appColors: com.aiosman.ravenow.AppThemeData, + amountColor: Color? = null +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + fontSize = 14.sp, + color = appColors.secondaryText + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + // 星形图标(参考 iOS 版本) + Icon( + imageVector = Icons.Default.Star, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = Color(0xFFFFD700) // 黄色 + ) + + Text( + text = "${amount.formatNumber()}", + fontSize = 16.sp, + fontWeight = FontWeight.W600, + color = amountColor ?: appColors.text + ) + + Text( + text = stringResource(R.string.pai_coin), + fontSize = 14.sp, + color = appColors.secondaryText + ) + } + } +} + +/** + * 格式化数字,添加千位分隔符 + */ +private fun Int.formatNumber(): String { + return this.toString().reversed().chunked(3).joinToString(",").reversed() +} + diff --git a/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput.kt b/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput.kt index 3f4c97b..b979432 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput.kt @@ -67,6 +67,13 @@ fun FormTextInput( .let { if (error != null) { it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp)) + } else if (background != null && background == Color.White) { + // 如果传入白色背景,添加灰色边框 + it.border( + 1.dp, + Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f), + RoundedCornerShape(25.dp) + ) } else { it } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput2.kt b/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput2.kt index cf7bc18..6aa17b1 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput2.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/composables/form/FormTextInput2.kt @@ -68,6 +68,13 @@ fun FormTextInput2( .let { if (error != null) { it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp)) + } else if (background != null && background == Color.White) { + // 如果传入白色背景,添加灰色边框 + it.border( + 1.dp, + Color(red = 124f / 255f, green = 116f / 255f, blue = 128f / 255f, alpha = 0.08f), + RoundedCornerShape(25.dp) + ) } else { it } diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 3146036..9002cb5 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -376,6 +376,11 @@ 余额不足 成员数量超过上限(%1$d) 派币 + 需要消耗 + 支付后余额 + 派币余额不足 + 去充值 + 确认消费 连接世界,从关注开始 不如从一个 Agent 开始认识这世界? 去探索 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3e8377..5acf512 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -369,6 +369,11 @@ Insufficient balance Member count exceeds the limit (%1$d) Pai Coin + Cost Required + Balance After + Insufficient Pai Coin Balance + Go Recharge + Confirm Consumption Connect the world, start by following Why not start exploring the world with an Agent? Explore