新增创建群聊的费用和人数上限功能
- **创建群聊费用:** - 创建群聊现在会根据后台配置的积分规则扣除相应费用(派币)。 - 在创建页面会显示当前余额和所需费用。 - 创建时会弹出确认弹窗,显示费用、当前余额和扣除后余额。 - 如果余额不足,将无法创建。 - **群聊人数上限:** - 新增创建群聊时的初始成员人数上限,该上限从后台动态获取。 - 在选择成员界面会显示当前已选人数和上限(例如 `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.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){
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user