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

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

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

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

View File

@@ -32,6 +32,7 @@ import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel
import com.aiosman.ravenow.ui.like.LikeNoticeViewModel import com.aiosman.ravenow.ui.like.LikeNoticeViewModel
import com.aiosman.ravenow.utils.Utils import com.aiosman.ravenow.utils.Utils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import com.aiosman.ravenow.im.OpenIMManager import com.aiosman.ravenow.im.OpenIMManager
import io.openim.android.sdk.OpenIMClient import io.openim.android.sdk.OpenIMClient
@@ -51,7 +52,7 @@ object AppState {
suspend fun initWithAccount(scope: CoroutineScope, context: Context) { suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
// 如果是游客模式,使用简化的初始化流程 // 如果是游客模式,使用简化的初始化流程
if (AppStore.isGuest) { if (AppStore.isGuest) {
initWithGuestAccount() initWithGuestAccount(scope)
return return
} }
@@ -90,18 +91,50 @@ object AppState {
} catch (e: Exception) { } catch (e: Exception) {
Log.e("AppState", "刷新积分失败: ${e.message}") Log.e("AppState", "刷新积分失败: ${e.message}")
} }
// 并行加载积分规则和房间规则配置(不阻塞主流程)
scope.launch {
try {
PointService.refreshPointsRules()
} catch (e: Exception) {
Log.e("AppState", "加载积分规则失败: ${e.message}")
}
}
scope.launch {
try {
PointService.refreshRoomMaxMembers()
} catch (e: Exception) {
Log.e("AppState", "加载房间规则失败: ${e.message}")
}
}
} }
/** /**
* 游客模式的简化初始化 * 游客模式的简化初始化
*/ */
private fun initWithGuestAccount() { private fun initWithGuestAccount(scope: CoroutineScope) {
// 游客模式下不初始化推送和TRTC // 游客模式下不初始化推送和TRTC
// 设置默认的用户信息 // 设置默认的用户信息
UserId = 0 UserId = 0
profile = null profile = null
enableChat = false enableChat = false
Log.d("AppState", "Guest mode initialized without push notifications and TRTC") Log.d("AppState", "Guest mode initialized without push notifications and TRTC")
// 游客模式下也加载规则配置(用于查看费用信息)
scope.launch {
try {
PointService.refreshPointsRules()
} catch (e: Exception) {
Log.e("AppState", "加载积分规则失败: ${e.message}")
}
}
scope.launch {
try {
PointService.refreshRoomMaxMembers()
} catch (e: Exception) {
Log.e("AppState", "加载房间规则失败: ${e.message}")
}
}
} }
private suspend fun initChat(context: Context){ private suspend fun initChat(context: Context){

View File

@@ -14,6 +14,16 @@ interface DictService {
* 获取字典列表 * 获取字典列表
*/ */
suspend fun getDistList(keys: List<String>): List<DictItem> suspend fun getDistList(keys: List<String>): List<DictItem>
/**
* 获取外部字典项
*/
suspend fun getOutsideDictByKey(key: String): DictItem
/**
* 获取外部字典列表
*/
suspend fun getOutsideDistList(keys: List<String>): List<DictItem>
} }
class DictServiceImpl : DictService { class DictServiceImpl : DictService {
@@ -26,4 +36,13 @@ class DictServiceImpl : DictService {
val resp = ApiClient.api.getDicts(keys.joinToString(",")) val resp = ApiClient.api.getDicts(keys.joinToString(","))
return resp.body()?.list ?: throw Exception("failed to get dict list") return resp.body()?.list ?: throw Exception("failed to get dict list")
} }
override suspend fun getOutsideDictByKey(key: String): DictItem {
val resp = ApiClient.api.getOutsideDict(key)
return resp.body()?.data ?: throw Exception("failed to get outside dict")
}
override suspend fun getOutsideDistList(keys: List<String>): List<DictItem> {
val resp = ApiClient.api.getOutsideDicts(keys.joinToString(","))
return resp.body()?.list ?: throw Exception("failed to get outside dict list")
}
} }

View File

@@ -26,6 +26,36 @@ object PointService {
private val dictService: DictService = DictServiceImpl() private val dictService: DictService = DictServiceImpl()
private val gson = Gson() private val gson = Gson()
/**
* 积分规则key常量
* 对应积分规则JSON中的key值
*/
object PointsRuleKey {
// 获得积分类型add
/** 每日登录奖励 */
const val DAILY_LOGIN = "daily_login"
/** 用户注册奖励 */
const val USER_REGISTER = "user_register"
// 消费积分类型sub
/** 添加Agent记忆 */
const val ADD_AGENT_MEMORY = "add_agent_memory"
/** 增加房间容量 */
const val ADD_ROOM_CAP = "add_room_cap"
/** 创建房间 */
const val CREATE_ROOM = "create_room"
/** 创建定时事件 */
const val CREATE_SCHEDULE_EVENT = "create_schedule_event"
/** 房间私密模式 */
const val ROOM_PRIVATE = "room_private"
/** Agent私密模式 */
const val SPEND_AGENT_PRIVATE = "spend_agent_private"
/** 自定义聊天背景 */
const val SPEND_CHAT_BACKGROUND = "spend_chat_background"
/** 房间记忆添加 */
const val SPEND_ROOM_MEMORY = "spend_room_memory"
}
sealed class RuleAmount { sealed class RuleAmount {
data class Fixed(val value: Int) : RuleAmount() data class Fixed(val value: Int) : RuleAmount()
data class Range(val min: Int, val max: Int) : RuleAmount() data class Range(val min: Int, val max: Int) : RuleAmount()
@@ -50,6 +80,64 @@ object PointService {
} }
} }
// ========== 群聊人数限制(字典 points-rule相关 ==========
/**
* 群聊人数限制配置
* @param defaultMaxTotal 初始最大人数(默认值)
* @param maxTotal 最大人数(上限)
*/
data class RoomMaxMembers(
val defaultMaxTotal: Int,
val maxTotal: Int
)
private val _roomMaxMembers = MutableStateFlow<RoomMaxMembers?>(null)
val roomMaxMembers: StateFlow<RoomMaxMembers?> = _roomMaxMembers.asStateFlow()
/**
* 刷新群聊人数限制配置(从外部字典表加载 points-rule
* 加载时机与 refreshPointsRules 一致
*/
suspend fun refreshRoomMaxMembers(key: String = "points-rule") {
withContext(Dispatchers.IO) {
try {
val dict = dictService.getOutsideDictByKey(key)
val config = parseRoomMaxMembers(dict)
_roomMaxMembers.value = config
} catch (_: Exception) {
_roomMaxMembers.value = null
}
}
}
/**
* 解析群聊人数限制配置
* 解析格式:{"room":{"default":{"max-total":5},"max":{"max-total":200}}}
*/
private fun parseRoomMaxMembers(dict: DictItem): RoomMaxMembers? {
val raw = dict.value
val jsonStr = when (raw) {
is String -> raw
else -> gson.toJson(raw)
}
return try {
val root = JsonParser.parseString(jsonStr).asJsonObject
val roomObj = root.getAsJsonObject("room")
val defaultObj = roomObj?.getAsJsonObject("default")
val maxObj = roomObj?.getAsJsonObject("max")
val defaultMaxTotal = defaultObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 5
val maxTotal = maxObj?.get("max-total")?.takeIf { it.isJsonPrimitive }?.asInt ?: 200
RoomMaxMembers(
defaultMaxTotal = defaultMaxTotal,
maxTotal = maxTotal
)
} catch (_: Exception) {
null
}
}
private fun parsePointsRules(dict: DictItem): PointsRules? { private fun parsePointsRules(dict: DictItem): PointsRules? {
val raw = dict.value val raw = dict.value
val jsonStr = when (raw) { val jsonStr = when (raw) {

View File

@@ -1335,6 +1335,16 @@ interface RaveNowAPI {
@Query("keys") keys: String @Query("keys") keys: String
): Response<ListContainer<DictItem>> ): Response<ListContainer<DictItem>>
@GET("/outside/dict")
suspend fun getOutsideDict(
@Query("key") key: String
): Response<DataContainer<DictItem>>
@GET("/outside/dicts")
suspend fun getOutsideDicts(
@Query("keys") keys: String
): Response<ListContainer<DictItem>>
@POST("captcha/generate") @POST("captcha/generate")
suspend fun generateCaptcha( suspend fun generateCaptcha(
@Body body: CaptchaRequestBody @Body body: CaptchaRequestBody

View File

@@ -3,6 +3,7 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -14,11 +15,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
@@ -27,11 +32,15 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.LocalAppTheme
import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.LocalNavController
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
import com.aiosman.ravenow.data.PointService
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.TabItem import com.aiosman.ravenow.ui.composables.TabItem
@@ -91,6 +100,19 @@ fun CreateGroupChatScreen() {
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
// 获取费用和余额信息
val pointsRules by PointService.pointsRules.collectAsState(initial = null)
val pointsBalance by PointService.pointsBalance.collectAsState(initial = null)
val roomMaxMembers by PointService.roomMaxMembers.collectAsState(initial = null)
val cost = CreateGroupChatViewModel.getCreateRoomCost()
val currentBalance = CreateGroupChatViewModel.getCurrentBalance()
val balanceAfterCost = CreateGroupChatViewModel.calculateBalanceAfterCost(cost)
val isBalanceSufficient = CreateGroupChatViewModel.isBalanceSufficient(cost)
// 获取群聊初始上限
val maxMemberLimit = roomMaxMembers?.defaultMaxTotal ?: 5
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent) systemUiController.setNavigationBarColor(Color.Transparent)
} }
@@ -367,33 +389,47 @@ fun CreateGroupChatScreen() {
} }
} }
// Tab切换 // Tab切换和成员数量显示
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.CenterVertically
) { ) {
TabItem( // Tab左对齐
text = stringResource(R.string.chat_ai), Row(
isSelected = pagerState.currentPage == 0, horizontalArrangement = Arrangement.Start,
onClick = { verticalAlignment = Alignment.CenterVertically
scope.launch { ) {
pagerState.animateScrollToPage(0) TabItem(
text = stringResource(R.string.chat_ai),
isSelected = pagerState.currentPage == 0,
onClick = {
scope.launch {
pagerState.animateScrollToPage(0)
}
} }
} )
) TabSpacer()
TabSpacer() TabItem(
TabItem( text = stringResource(R.string.chat_friend),
text = stringResource(R.string.chat_friend), isSelected = pagerState.currentPage == 1,
isSelected = pagerState.currentPage == 1, onClick = {
onClick = { scope.launch {
scope.launch { pagerState.animateScrollToPage(1)
pagerState.animateScrollToPage(1) }
} }
} )
}
// 成员数量显示右对齐x/x格式
Text(
text = "${selectedMembers.size}/$maxMemberLimit",
fontSize = 14.sp,
color = if (selectedMembers.size > maxMemberLimit) AppColors.error else AppColors.secondaryText,
fontWeight = FontWeight.W500
) )
} }
@@ -436,26 +472,60 @@ fun CreateGroupChatScreen() {
} }
} }
// 余额和扣减积分显示(创建按钮上方)
val buttonTopPadding = if (cost > 0) 4.dp else 16.dp
if (cost > 0) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.Center
) {
Text(
text = "${stringResource(R.string.create_group_chat_current_balance)}: ${currentBalance.formatNumber()} ${stringResource(R.string.pai_coin)}",
fontSize = 12.sp,
color = AppColors.secondaryText
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = "${stringResource(R.string.create_group_chat_required_cost)} ${cost.formatNumber()} ${stringResource(R.string.pai_coin)}",
fontSize = 12.sp,
color = AppColors.secondaryText
)
}
}
// 创建群聊按钮 - 固定在底部 // 创建群聊按钮 - 固定在底部
Button( Button(
onClick = { onClick = {
// 创建群聊逻辑 // 创建群聊逻辑
if (selectedMembers.isNotEmpty()) { if (selectedMembers.isNotEmpty()) {
scope.launch { // 检查是否超过上限
val success = CreateGroupChatViewModel.createGroupChat( if (selectedMembers.size > maxMemberLimit) {
groupName = groupName.text, CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
selectedMembers = selectedMembers, return@Button
context = context }
) // 如果费用大于0显示确认弹窗
if (success) { if (cost > 0) {
navController.popBackStack() CreateGroupChatViewModel.showConfirmDialog()
} else {
// 费用为0直接创建
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
)
if (success) {
navController.popBackStack()
}
} }
} }
} }
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = navigationBarPadding + 16.dp), .padding(start = 16.dp, end = 16.dp, top = buttonTopPadding, bottom = navigationBarPadding + 16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main, containerColor = AppColors.main,
contentColor = AppColors.mainText, contentColor = AppColors.mainText,
@@ -482,6 +552,38 @@ fun CreateGroupChatScreen() {
} }
// 消费确认弹窗
if (CreateGroupChatViewModel.showConfirmDialog) {
CreateGroupChatConfirmDialog(
cost = cost,
currentBalance = currentBalance,
balanceAfterCost = balanceAfterCost,
isBalanceSufficient = isBalanceSufficient,
onConfirm = {
// 检查是否超过上限
if (selectedMembers.size > maxMemberLimit) {
CreateGroupChatViewModel.hideConfirmDialog()
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
} else {
CreateGroupChatViewModel.hideConfirmDialog()
scope.launch {
val success = CreateGroupChatViewModel.createGroupChat(
groupName = groupName.text,
selectedMembers = selectedMembers,
context = context
)
if (success) {
navController.popBackStack()
}
}
}
},
onCancel = {
CreateGroupChatViewModel.hideConfirmDialog()
}
)
}
// 居中显示的错误提示弹窗 // 居中显示的错误提示弹窗
CreateGroupChatViewModel.errorMessage?.let { error -> CreateGroupChatViewModel.errorMessage?.let { error ->
Box( Box(
@@ -496,7 +598,7 @@ fun CreateGroupChatScreen() {
horizontalAlignment = Alignment.CenterHorizontally, // 水平居中 horizontalAlignment = Alignment.CenterHorizontally, // 水平居中
verticalArrangement = Arrangement.Center // 垂直居中 verticalArrangement = Arrangement.Center // 垂直居中
) { ) {
androidx.compose.material3.Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.8f), .fillMaxWidth(0.8f),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
@@ -508,7 +610,7 @@ fun CreateGroupChatScreen() {
.fillMaxWidth(), .fillMaxWidth(),
color = Color.Red, color = Color.Red,
fontSize = 14.sp, fontSize = 14.sp,
textAlign = androidx.compose.ui.text.style.TextAlign.Center textAlign = TextAlign.Center
) )
} }
} }
@@ -516,3 +618,219 @@ fun CreateGroupChatScreen() {
} }
} }
} }
/**
* 创建群聊消费确认弹窗
*/
@Composable
fun CreateGroupChatConfirmDialog(
cost: Int,
currentBalance: Int,
balanceAfterCost: Int,
isBalanceSufficient: Boolean,
onConfirm: () -> Unit,
onCancel: () -> Unit
) {
val AppColors = LocalAppTheme.current
Dialog(
onDismissRequest = onCancel,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Card(
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(16.dp),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 硬币图标(使用文本代替,实际项目中可以使用图片资源)
Box(
modifier = Modifier
.size(64.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFD700), // 金色
Color(0xFFFFA500) // 橙色
)
),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Text(
text = "pai",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(16.dp))
// 标题
Text(
text = stringResource(R.string.create_group_chat_confirm_title),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = AppColors.text
)
Spacer(modifier = Modifier.height(24.dp))
// 需要消耗
CostInfoRow(
label = stringResource(R.string.create_group_chat_required_cost),
amount = cost,
AppColors = AppColors
)
Spacer(modifier = Modifier.height(12.dp))
// 当前余额
CostInfoRow(
label = stringResource(R.string.create_group_chat_current_balance),
amount = currentBalance,
AppColors = AppColors
)
Spacer(modifier = Modifier.height(12.dp))
// 消耗后余额
CostInfoRow(
label = stringResource(R.string.create_group_chat_balance_after),
amount = balanceAfterCost,
AppColors = AppColors
)
// 余额不足提示
if (!isBalanceSufficient) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(R.string.create_group_chat_insufficient_balance),
color = Color.Red,
fontSize = 14.sp
)
}
Spacer(modifier = Modifier.height(24.dp))
// 按钮行
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 取消按钮
OutlinedButton(
onClick = onCancel,
modifier = Modifier
.weight(1f)
.height(48.dp)
.border(
width = 1.dp,
color = AppColors.secondaryText.copy(alpha = 0.3f),
shape = RoundedCornerShape(24.dp)
),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = AppColors.text
),
shape = RoundedCornerShape(24.dp)
) {
Text(
text = stringResource(R.string.cancel),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
// 确认消耗按钮
Button(
onClick = onConfirm,
modifier = Modifier
.weight(1f)
.height(48.dp),
enabled = isBalanceSufficient,
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main,
contentColor = AppColors.mainText,
disabledContainerColor = AppColors.disabledBackground,
disabledContentColor = AppColors.text
),
shape = RoundedCornerShape(24.dp)
) {
Text(
text = stringResource(R.string.create_group_chat_confirm_consume),
fontSize = 16.sp,
fontWeight = FontWeight.W600
)
}
}
}
}
}
}
/**
* 费用信息行组件
*/
@Composable
fun CostInfoRow(
label: String,
amount: Int,
AppColors: com.aiosman.ravenow.AppThemeData
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
fontSize = 14.sp,
color = AppColors.text
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
// 小硬币图标(使用简单的圆形)
Box(
modifier = Modifier
.size(16.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFFFFD700),
Color(0xFFFFA500)
)
),
shape = CircleShape
)
)
Text(
text = "${amount.formatNumber()} ${stringResource(R.string.pai_coin)}",
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = AppColors.text
)
}
}
}
/**
* 格式化数字,添加千位分隔符
*/
fun Int.formatNumber(): String {
return this.toString().reversed().chunked(3).joinToString(",").reversed()
}

View File

@@ -16,6 +16,7 @@ import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.AccountNotice import com.aiosman.ravenow.data.AccountNotice
import com.aiosman.ravenow.data.AccountService import com.aiosman.ravenow.data.AccountService
import com.aiosman.ravenow.data.AccountServiceImpl import com.aiosman.ravenow.data.AccountServiceImpl
import com.aiosman.ravenow.data.PointService
import com.aiosman.ravenow.data.UserService import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.UserServiceImpl import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.R import com.aiosman.ravenow.R
@@ -36,6 +37,7 @@ object CreateGroupChatViewModel : ViewModel() {
// 状态管理 // 状态管理
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var errorMessage by mutableStateOf<String?>(null) var errorMessage by mutableStateOf<String?>(null)
var showConfirmDialog by mutableStateOf(false)
// 创建群聊 // 创建群聊
suspend fun createGroupChat( suspend fun createGroupChat(
@@ -76,6 +78,11 @@ object CreateGroupChatViewModel : ViewModel() {
} }
} }
// 显示错误信息(公开方法)
fun showError(message: String) {
showToast(message)
}
// 清除错误信息 // 清除错误信息
fun clearError() { fun clearError() {
errorMessage = null errorMessage = null
@@ -107,4 +114,59 @@ object CreateGroupChatViewModel : ViewModel() {
addSelectedMember(member, selectedMemberIds, selectedMembers) addSelectedMember(member, selectedMemberIds, selectedMembers)
} }
} }
/**
* 获取创建群聊的费用
* @return 费用金额,如果无法获取则返回 0
*/
fun getCreateRoomCost(): Int {
val rules = PointService.pointsRules.value
val costRule = rules?.sub?.get(PointService.PointsRuleKey.CREATE_ROOM)
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
}
/**
* 显示确认弹窗
*/
fun showConfirmDialog() {
showConfirmDialog = true
}
/**
* 隐藏确认弹窗
*/
fun hideConfirmDialog() {
showConfirmDialog = false
}
} }

View File

@@ -23,10 +23,11 @@ object PointsViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
try { try {
loading = true loading = true
// 并行预加载积分定价表不影响UI // 并行预加载积分定价表和群聊人数限制不影响UI
launch { launch {
try { try {
PointService.refreshPointsRules() PointService.refreshPointsRules()
PointService.refreshRoomMaxMembers()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("PointsViewModel", "refresh rules error", e) Log.e("PointsViewModel", "refresh rules error", e)
} }

View File

@@ -368,6 +368,14 @@
<string name="recharge_pai_coin">充值派币</string> <string name="recharge_pai_coin">充值派币</string>
<string name="recharge_pai_coin_desc">多种套餐可选,立即充值</string> <string name="recharge_pai_coin_desc">多种套餐可选,立即充值</string>
<string name="create_group_chat_failed">创建群聊失败: %1$s</string> <string name="create_group_chat_failed">创建群聊失败: %1$s</string>
<string name="create_group_chat_confirm_title">创建群聊确认</string>
<string name="create_group_chat_required_cost">需要消耗:</string>
<string name="create_group_chat_current_balance">当前余额</string>
<string name="create_group_chat_balance_after">消耗后余额:</string>
<string name="create_group_chat_confirm_consume">确认消耗</string>
<string name="create_group_chat_insufficient_balance">余额不足</string>
<string name="create_group_chat_exceed_limit">成员数量超过上限(%1$d</string>
<string name="pai_coin">派币</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

@@ -361,6 +361,14 @@
<string name="recharge_pai_coin">Recharge Pai Coin</string> <string name="recharge_pai_coin">Recharge Pai Coin</string>
<string name="recharge_pai_coin_desc">Multiple packages available, recharge now</string> <string name="recharge_pai_coin_desc">Multiple packages available, recharge now</string>
<string name="create_group_chat_failed">Failed to create group chat: %1$s</string> <string name="create_group_chat_failed">Failed to create group chat: %1$s</string>
<string name="create_group_chat_confirm_title">Create Group Chat</string>
<string name="create_group_chat_required_cost">Required consumption:</string>
<string name="create_group_chat_current_balance">Current balance</string>
<string name="create_group_chat_balance_after">Balance after consumption:</string>
<string name="create_group_chat_confirm_consume">Confirm consumption</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="pai_coin">Pai Coin</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>