新增创建群聊的费用和人数上限功能

- **创建群聊费用:**
  - 创建群聊现在会根据后台配置的积分规则扣除相应费用(派币)。
  - 在创建页面会显示当前余额和所需费用。
  - 创建时会弹出确认弹窗,显示费用、当前余额和扣除后余额。
  - 如果余额不足,将无法创建。

- **群聊人数上限:**
  - 新增创建群聊时的初始成员人数上限,该上限从后台动态获取。
  - 在选择成员界面会显示当前已选人数和上限(例如 `5/10`)。
  - 如果选择的成员超过上限,会提示错误并且无法创建。

- **后台数据加载:**
  - 新增了从外部字典表 (`/outside/dict`) 获取配置的接口和逻辑,用于加载积分规则和群聊人数限制。
  - App启动时会预加载这些配置,以确保创建群聊时能正确显示费用和人数限制。
This commit is contained in:
2025-11-12 18:10:40 +08:00
parent ca16d54823
commit 464d0adb19
7 changed files with 517 additions and 151 deletions

View File

@@ -33,10 +33,13 @@ import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.ActionButton import com.aiosman.ravenow.ui.composables.ActionButton
import com.aiosman.ravenow.ui.composables.CustomAsyncImage 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.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.form.FormTextInput import com.aiosman.ravenow.ui.composables.form.FormTextInput
import com.aiosman.ravenow.ui.composables.form.FormTextInput2 import com.aiosman.ravenow.ui.composables.form.FormTextInput2
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.data.PointService
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
@@ -71,23 +74,30 @@ fun AiPromptEditScreen(
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// 获取积分规则和余额
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
// 计算是否需要付费 // 计算是否需要付费
val needsPayment = viewModel.needsPrivacyPayment() 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(color = appColors.background), .background(color = Color(0xFFFAFAFB)),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
StatusBarSpacer() StatusBarSpacer()
// 顶部导航栏 // 顶部导航栏
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(color = appColors.background) .background(color = Color(0xFFFAFAFB))
.padding(horizontal = 14.dp, vertical = 16.dp) .padding(horizontal = 14.dp, vertical = 16.dp)
) { ) {
Row( Row(
@@ -117,15 +127,15 @@ fun AiPromptEditScreen(
) )
} }
} }
Spacer(modifier = Modifier.height(1.dp)) Spacer(modifier = Modifier.height(1.dp))
// 内容区域 // 内容区域
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.background(appColors.background) .background(Color(0xFFFAFAFB))
) { ) {
// 头像选择 // 头像选择
Column( Column(
@@ -195,9 +205,9 @@ fun AiPromptEditScreen(
} }
} }
} }
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(18.dp))
// 名称输入 // 名称输入
Column( Column(
modifier = Modifier modifier = Modifier
@@ -214,7 +224,7 @@ fun AiPromptEditScreen(
FormTextInput( FormTextInput(
value = viewModel.title, value = viewModel.title,
hint = stringResource(R.string.agent_name_hint_1), hint = stringResource(R.string.agent_name_hint_1),
background = appColors.inputBackground2, background = Color.White,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { value -> ) { value ->
viewModel.title = value viewModel.title = value
@@ -222,7 +232,7 @@ fun AiPromptEditScreen(
} }
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(18.dp))
// 描述输入 // 描述输入
Column( Column(
modifier = Modifier modifier = Modifier
@@ -239,15 +249,15 @@ fun AiPromptEditScreen(
FormTextInput2( FormTextInput2(
value = viewModel.desc, value = viewModel.desc,
hint = stringResource(R.string.agent_desc_hint), hint = stringResource(R.string.agent_desc_hint),
background = appColors.inputBackground2, background = Color.White,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { value -> ) { value ->
viewModel.desc = value viewModel.desc = value
} }
} }
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(18.dp))
// 设定权限区域 // 设定权限区域
Column( Column(
modifier = Modifier modifier = Modifier
@@ -261,13 +271,18 @@ fun AiPromptEditScreen(
fontWeight = FontWeight.W600 fontWeight = FontWeight.W600
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// 公开/私有切换 // 公开/私有切换
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(25.dp))
.background(appColors.inputBackground2) .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), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@@ -298,58 +313,89 @@ fun AiPromptEditScreen(
modifier = Modifier.size(width = 64.dp, height = 28.dp) modifier = Modifier.size(width = 64.dp, height = 28.dp)
) )
} }
// 首次解锁AI权限提示 // 首次解锁AI权限提示
if (needsPayment && !viewModel.paidForPrivacyEdit) { if (needsPayment && !viewModel.paidForPrivacyEdit && privacyCost > 0) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// 主要内容容器(去掉阴影)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(16.dp))
.background(appColors.inputBackground2.copy(alpha = 0.9f)) .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) .padding(12.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), 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( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text( Text(
text = "首次解锁 AI 权限", text = "首次解锁Ai权限",
fontSize = 13.sp, fontSize = 13.sp,
color = appColors.text, color = Color(red = 172f / 255f, green = 127f / 255f, blue = 94f / 255f),
fontWeight = FontWeight.W500 fontWeight = FontWeight.W500
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
Text( Text(
text = "将消耗", text = "将消耗",
fontSize = 12.sp, 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(
text = "$privacyCost", text = "$privacyCost",
fontSize = 12.sp, fontSize = 12.sp,
color = appColors.brandColorsColor, color = Color(red = 1f, green = 141f / 255f, blue = 40f / 255f)
fontWeight = FontWeight.W500
) )
Spacer(modifier = Modifier.width(2.dp)) // 小硬币图标
Text( Box(
text = "钥匙", modifier = Modifier
fontSize = 12.sp, .size(16.dp)
color = appColors.brandColorsColor, .background(
fontWeight = FontWeight.W500 brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFD700),
Color(0xFFFFA500)
)
),
shape = CircleShape
)
) )
Spacer(modifier = Modifier.width(4.dp))
Text( Text(
text = "解锁后可随时切换", text = "解锁后可随时切换",
fontSize = 12.sp, 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( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(color = appColors.background) .background(color = Color(0xFFFAFAFB))
.padding(horizontal = 16.dp, vertical = 16.dp) .padding(horizontal = 16.dp, vertical = 16.dp)
) { ) {
ActionButton( ActionButton(
@@ -388,13 +424,13 @@ fun AiPromptEditScreen(
errorMessage = validationError errorMessage = validationError
return@ActionButton return@ActionButton
} }
// 检查是否需要付费确认 // 检查是否需要付费确认
if (needsPayment && !viewModel.paidForPrivacyEdit) { if (needsPayment && !viewModel.paidForPrivacyEdit) {
showPrivacyConfirmDialog = true showPrivacyConfirmDialog = true
return@ActionButton return@ActionButton
} }
// 执行保存 // 执行保存
scope.launch { scope.launch {
try { try {
@@ -407,44 +443,35 @@ fun AiPromptEditScreen(
} }
} }
} }
// 隐私权限付费确认对话框 // 隐私权限付费确认对话框
if (showPrivacyConfirmDialog) { if (showPrivacyConfirmDialog) {
// TODO: 实现付费确认对话框 PointsPaymentDialog(
// 暂时直接切换,后续可以添加积分检查和扣减逻辑 cost = privacyCost,
androidx.compose.material3.AlertDialog( currentBalance = currentBalance,
onDismissRequest = { showPrivacyConfirmDialog = false }, balanceAfterCost = balanceAfterCost,
title = { Text("升级隐私权限") }, isBalanceSufficient = isBalanceSufficient,
text = { Text("首次切换智能体的公开/私有状态需要支付一次性费用。支付后可自由在公有/私有之间切换,后续不再扣费。") }, onConfirm = {
confirmButton = { showPrivacyConfirmDialog = false
androidx.compose.material3.TextButton( scope.launch {
onClick = { try {
showPrivacyConfirmDialog = false viewModel.isPublic = false
scope.launch { viewModel.updatePrompt(context)
try { viewModel.paidForPrivacyEdit = true
viewModel.isPublic = false navController.navigateUp()
viewModel.updatePrompt(context) } catch (e: Exception) {
viewModel.paidForPrivacyEdit = true errorMessage = e.message ?: "保存失败"
navController.navigateUp()
} catch (e: Exception) {
errorMessage = e.message ?: "保存失败"
}
}
} }
) {
Text("确认支付")
} }
}, },
dismissButton = { onCancel = {
androidx.compose.material3.TextButton( showPrivacyConfirmDialog = false
onClick = { showPrivacyConfirmDialog = false } },
) { title = "首次解锁AI权限",
Text("取消") description = "将消耗 $privacyCost 派币解锁后可随时切换"
}
}
) )
} }
// 错误提示 // 错误提示
errorMessage?.let { error -> errorMessage?.let { error ->
LaunchedEffect(error) { LaunchedEffect(error) {
@@ -454,72 +481,3 @@ fun AiPromptEditScreen(
// TODO: 显示Toast或Snackbar // 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)
)
}
}
}
}
}

View File

@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aiosman.ravenow.data.UploadImage import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.PointService
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -145,6 +146,47 @@ class AiPromptEditViewModel : ViewModel() {
return originalIsPublic == true && isPublic == false 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
}
/** /**
* 清空数据 * 清空数据
*/ */

View File

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

View File

@@ -67,6 +67,13 @@ fun FormTextInput(
.let { .let {
if (error != null) { if (error != null) {
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp)) 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 { } else {
it it
} }

View File

@@ -68,6 +68,13 @@ fun FormTextInput2(
.let { .let {
if (error != null) { if (error != null) {
it.border(1.dp, AppColors.error, RoundedCornerShape(24.dp)) 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 { } else {
it it
} }

View File

@@ -376,6 +376,11 @@
<string name="create_group_chat_insufficient_balance">余额不足</string> <string name="create_group_chat_insufficient_balance">余额不足</string>
<string name="create_group_chat_exceed_limit">成员数量超过上限(%1$d</string> <string name="create_group_chat_exceed_limit">成员数量超过上限(%1$d</string>
<string name="pai_coin">派币</string> <string name="pai_coin">派币</string>
<string name="cost_required">需要消耗</string>
<string name="balance_after">支付后余额</string>
<string name="insufficient_pai_coin_balance">派币余额不足</string>
<string name="go_recharge">去充值</string>
<string name="confirm_consumption">确认消费</string>
<string name="connect_world_start_following">连接世界,从关注开始</string> <string name="connect_world_start_following">连接世界,从关注开始</string>
<string name="why_not_start_with_agent">不如从一个 Agent 开始认识这世界?</string> <string name="why_not_start_with_agent">不如从一个 Agent 开始认识这世界?</string>
<string name="explore">去探索</string> <string name="explore">去探索</string>

View File

@@ -369,6 +369,11 @@
<string name="create_group_chat_insufficient_balance">Insufficient balance</string> <string name="create_group_chat_insufficient_balance">Insufficient balance</string>
<string name="create_group_chat_exceed_limit">Member count exceeds the limit (%1$d)</string> <string name="create_group_chat_exceed_limit">Member count exceeds the limit (%1$d)</string>
<string name="pai_coin">Pai Coin</string> <string name="pai_coin">Pai Coin</string>
<string name="cost_required">Cost Required</string>
<string name="balance_after">Balance After</string>
<string name="insufficient_pai_coin_balance">Insufficient Pai Coin Balance</string>
<string name="go_recharge">Go Recharge</string>
<string name="confirm_consumption">Confirm Consumption</string>
<string name="connect_world_start_following">Connect the world, start by following</string> <string name="connect_world_start_following">Connect the world, start by following</string>
<string name="why_not_start_with_agent">Why not start exploring the world with an Agent?</string> <string name="why_not_start_with_agent">Why not start exploring the world with an Agent?</string>
<string name="explore">Explore</string> <string name="explore">Explore</string>