新增创建群聊的费用和人数上限功能
- **创建群聊费用:** - 创建群聊现在会根据后台配置的积分规则扣除相应费用(派币)。 - 在创建页面会显示当前余额和所需费用。 - 创建时会弹出确认弹窗,显示费用、当前余额和扣除后余额。 - 如果余额不足,将无法创建。 - **群聊人数上限:** - 新增创建群聊时的初始成员人数上限,该上限从后台动态获取。 - 在选择成员界面会显示当前已选人数和上限(例如 `5/10`)。 - 如果选择的成员超过上限,会提示错误并且无法创建。 - **后台数据加载:** - 新增了从外部字典表 (`/outside/dict`) 获取配置的接口和逻辑,用于加载积分规则和群聊人数限制。 - App启动时会预加载这些配置,以确保创建群聊时能正确显示费用和人数限制。
This commit is contained in:
@@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空数据
|
* 清空数据
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user