新增创建群聊的费用和人数上限功能
- **创建群聊费用:** - 创建群聊现在会根据后台配置的积分规则扣除相应费用(派币)。 - 在创建页面会显示当前余额和所需费用。 - 创建时会弹出确认弹窗,显示费用、当前余额和扣除后余额。 - 如果余额不足,将无法创建。 - **群聊人数上限:** - 新增创建群聊时的初始成员人数上限,该上限从后台动态获取。 - 在选择成员界面会显示当前已选人数和上限(例如 `5/10`)。 - 如果选择的成员超过上限,会提示错误并且无法创建。 - **后台数据加载:** - 新增了从外部字典表 (`/outside/dict`) 获取配置的接口和逻辑,用于加载积分规则和群聊人数限制。 - App启动时会预加载这些配置,以确保创建群聊时能正确显示费用和人数限制。
This commit is contained in:
@@ -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.utils.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.aiosman.ravenow.im.OpenIMManager
|
||||
import io.openim.android.sdk.OpenIMClient
|
||||
@@ -51,7 +52,7 @@ object AppState {
|
||||
suspend fun initWithAccount(scope: CoroutineScope, context: Context) {
|
||||
// 如果是游客模式,使用简化的初始化流程
|
||||
if (AppStore.isGuest) {
|
||||
initWithGuestAccount()
|
||||
initWithGuestAccount(scope)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,18 +91,50 @@ object AppState {
|
||||
} catch (e: Exception) {
|
||||
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
|
||||
// 设置默认的用户信息
|
||||
UserId = 0
|
||||
profile = null
|
||||
enableChat = false
|
||||
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){
|
||||
|
||||
@@ -14,6 +14,16 @@ interface DictService {
|
||||
* 获取字典列表
|
||||
*/
|
||||
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 {
|
||||
@@ -26,4 +36,13 @@ class DictServiceImpl : DictService {
|
||||
val resp = ApiClient.api.getDicts(keys.joinToString(","))
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,36 @@ object PointService {
|
||||
private val dictService: DictService = DictServiceImpl()
|
||||
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 {
|
||||
data class Fixed(val value: 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? {
|
||||
val raw = dict.value
|
||||
val jsonStr = when (raw) {
|
||||
|
||||
@@ -1335,6 +1335,16 @@ interface RaveNowAPI {
|
||||
@Query("keys") keys: String
|
||||
): 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")
|
||||
suspend fun generateCaptcha(
|
||||
@Body body: CaptchaRequestBody
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.aiosman.ravenow.ui.group
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
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.text.font.FontWeight
|
||||
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.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.ravenow.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.ravenow.ui.composables.TabItem
|
||||
@@ -91,6 +100,19 @@ fun CreateGroupChatScreen() {
|
||||
|
||||
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) {
|
||||
systemUiController.setNavigationBarColor(Color.Transparent)
|
||||
}
|
||||
@@ -367,33 +389,47 @@ fun CreateGroupChatScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
// Tab切换和成员数量显示
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Bottom
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_ai),
|
||||
isSelected = pagerState.currentPage == 0,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(0)
|
||||
// Tab左对齐
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_ai),
|
||||
isSelected = pagerState.currentPage == 0,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
TabSpacer()
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_friend),
|
||||
isSelected = pagerState.currentPage == 1,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(1)
|
||||
)
|
||||
TabSpacer()
|
||||
TabItem(
|
||||
text = stringResource(R.string.chat_friend),
|
||||
isSelected = pagerState.currentPage == 1,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
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(
|
||||
onClick = {
|
||||
// 创建群聊逻辑
|
||||
if (selectedMembers.isNotEmpty()) {
|
||||
scope.launch {
|
||||
val success = CreateGroupChatViewModel.createGroupChat(
|
||||
groupName = groupName.text,
|
||||
selectedMembers = selectedMembers,
|
||||
context = context
|
||||
)
|
||||
if (success) {
|
||||
navController.popBackStack()
|
||||
// 检查是否超过上限
|
||||
if (selectedMembers.size > maxMemberLimit) {
|
||||
CreateGroupChatViewModel.showError(context.getString(R.string.create_group_chat_exceed_limit, maxMemberLimit))
|
||||
return@Button
|
||||
}
|
||||
// 如果费用大于0,显示确认弹窗
|
||||
if (cost > 0) {
|
||||
CreateGroupChatViewModel.showConfirmDialog()
|
||||
} else {
|
||||
// 费用为0,直接创建
|
||||
scope.launch {
|
||||
val success = CreateGroupChatViewModel.createGroupChat(
|
||||
groupName = groupName.text,
|
||||
selectedMembers = selectedMembers,
|
||||
context = context
|
||||
)
|
||||
if (success) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.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(
|
||||
containerColor = AppColors.main,
|
||||
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 ->
|
||||
Box(
|
||||
@@ -496,7 +598,7 @@ fun CreateGroupChatScreen() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally, // 水平居中
|
||||
verticalArrangement = Arrangement.Center // 垂直居中
|
||||
) {
|
||||
androidx.compose.material3.Card(
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.8f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
@@ -508,7 +610,7 @@ fun CreateGroupChatScreen() {
|
||||
.fillMaxWidth(),
|
||||
color = Color.Red,
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.aiosman.ravenow.ConstVars
|
||||
import com.aiosman.ravenow.data.AccountNotice
|
||||
import com.aiosman.ravenow.data.AccountService
|
||||
import com.aiosman.ravenow.data.AccountServiceImpl
|
||||
import com.aiosman.ravenow.data.PointService
|
||||
import com.aiosman.ravenow.data.UserService
|
||||
import com.aiosman.ravenow.data.UserServiceImpl
|
||||
import com.aiosman.ravenow.R
|
||||
@@ -36,6 +37,7 @@ object CreateGroupChatViewModel : ViewModel() {
|
||||
// 状态管理
|
||||
var isLoading by mutableStateOf(false)
|
||||
var errorMessage by mutableStateOf<String?>(null)
|
||||
var showConfirmDialog by mutableStateOf(false)
|
||||
|
||||
// 创建群聊
|
||||
suspend fun createGroupChat(
|
||||
@@ -76,6 +78,11 @@ object CreateGroupChatViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误信息(公开方法)
|
||||
fun showError(message: String) {
|
||||
showToast(message)
|
||||
}
|
||||
|
||||
// 清除错误信息
|
||||
fun clearError() {
|
||||
errorMessage = null
|
||||
@@ -107,4 +114,59 @@ object CreateGroupChatViewModel : ViewModel() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ object PointsViewModel : ViewModel() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
loading = true
|
||||
// 并行预加载积分定价表(不影响UI)
|
||||
// 并行预加载积分定价表和群聊人数限制(不影响UI)
|
||||
launch {
|
||||
try {
|
||||
PointService.refreshPointsRules()
|
||||
PointService.refreshRoomMaxMembers()
|
||||
} catch (e: Exception) {
|
||||
Log.e("PointsViewModel", "refresh rules error", e)
|
||||
}
|
||||
|
||||
@@ -368,6 +368,14 @@
|
||||
<string name="recharge_pai_coin">充值派币</string>
|
||||
<string name="recharge_pai_coin_desc">多种套餐可选,立即充值</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="why_not_start_with_agent">不如从一个 Agent 开始认识这世界?</string>
|
||||
<string name="explore">去探索</string>
|
||||
|
||||
@@ -361,6 +361,14 @@
|
||||
<string name="recharge_pai_coin">Recharge Pai Coin</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_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="why_not_start_with_agent">Why not start exploring the world with an Agent?</string>
|
||||
<string name="explore">Explore</string>
|
||||
|
||||
Reference in New Issue
Block a user