diff --git a/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt b/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt index d7db439..43375a5 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/AgentService.kt @@ -73,7 +73,7 @@ data class Profile( @SerializedName("nickname") val nickname: String, @SerializedName("trtcUserId") - val trtcUserId: String, + val trtcUserId: String? = null, @SerializedName("username") val username: String ){ @@ -85,7 +85,7 @@ data class Profile( avatar = "${ApiClient.BASE_SERVER}$avatar", bio = bio, banner = "${ApiClient.BASE_SERVER}$banner", - trtcUserId = trtcUserId, + trtcUserId = trtcUserId ?: "", chatAIId = chatAIId, aiAccount = aiAccount ) diff --git a/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt b/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt index d7bf085..8ffda1f 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/RoomService.kt @@ -26,6 +26,22 @@ import com.aiosman.ravenow.entity.RoomRuleQuotaEntity import com.aiosman.ravenow.entity.UsersEntity import com.google.gson.annotations.SerializedName +/** + * 房间内的智能体信息(PromptTemplate) + */ +data class PromptTemplate( + @SerializedName("id") + val id: Int, + @SerializedName("openId") + val openId: String, + @SerializedName("title") + val title: String, + @SerializedName("desc") + val desc: String, + @SerializedName("avatar") + val avatar: String +) + data class Room( @SerializedName("id") val id: Int, @@ -51,12 +67,26 @@ data class Room( val creator: Creator, @SerializedName("userCount") val userCount: Int, + @SerializedName("totalMemberCount") + val totalMemberCount: Int? = null, @SerializedName("maxMemberLimit") val maxMemberLimit: Int, + @SerializedName("maxTotal") + val maxTotal: Int? = null, + @SerializedName("systemMaxTotal") + val systemMaxTotal: Int? = null, @SerializedName("canJoin") val canJoin: Boolean, @SerializedName("canJoinCode") val canJoinCode: Int, + @SerializedName("privateFeePaid") + val privateFeePaid: Boolean? = null, + @SerializedName("prompts") + val prompts: List? = null, + @SerializedName("createdAt") + val createdAt: String? = null, + @SerializedName("updatedAt") + val updatedAt: String? = null, @SerializedName("users") val users: List @@ -75,9 +105,24 @@ data class Room( allowInHot = allowInHot, creator = creator.toCreatorEntity(), userCount = userCount, + totalMemberCount = totalMemberCount, maxMemberLimit = maxMemberLimit, + maxTotal = maxTotal, + systemMaxTotal = systemMaxTotal, canJoin = canJoin, canJoinCode = canJoinCode, + privateFeePaid = privateFeePaid ?: false, + prompts = prompts?.map { + com.aiosman.ravenow.entity.PromptTemplateEntity( + id = it.id, + openId = it.openId, + title = it.title, + desc = it.desc, + avatar = it.avatar + ) + } ?: emptyList(), + createdAt = createdAt, + updatedAt = updatedAt, users = users.map { it.toUsersEntity() } ) } @@ -90,7 +135,7 @@ data class Creator( @SerializedName("userId") val userId: String, @SerializedName("trtcUserId") - val trtcUserId: String, + val trtcUserId: String? = null, @SerializedName("profile") val profile: Profile ){ @@ -98,7 +143,7 @@ data class Creator( return CreatorEntity( id = id, userId = userId, - trtcUserId = trtcUserId, + trtcUserId = trtcUserId ?: "", profile = profile.toProfileEntity() ) } @@ -110,7 +155,7 @@ data class Users( @SerializedName("userId") val userId: String, @SerializedName("trtcUserId") - val trtcUserId: String, + val trtcUserId: String? = null, @SerializedName("profile") val profile: Profile ){ diff --git a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt index 3e45392..24be872 100644 --- a/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/ravenow/data/api/RiderProAPI.kt @@ -1423,11 +1423,39 @@ interface RaveNowAPI { @POST("outside/rooms") suspend fun createGroupChat(@Body body: CreateGroupChatRequestBody): Response> + /** + * 获取房间列表 + * + * 支持游客和认证用户访问,根据用户类型返回不同的房间数据。 + * 游客模式返回公开推荐房间列表,认证用户模式返回用户可访问的群聊列表。 + * + * @param page 页码,默认 1 + * @param pageSize 每页数量,默认 20(游客模式最大50) + * @param roomId 房间ID,用于精确查询特定房间(仅认证用户) + * @param includeUsers 是否包含用户列表,默认false(仅认证用户) + * @param isRecommended 是否推荐过滤器:1=推荐,0=非推荐,null=不过滤(仅认证用户) + * @param roomType 房间类型过滤:all=公有私有都显示, public=只显示公有, private=只显示私有, created=只显示自己创建的, joined=只显示自己加入的(仅认证用户) + * @param search 搜索关键字,支持房间名称、描述、智能体名称模糊匹配 + * @param random 是否随机排序,字符串长度不为0则为true(传任意非空字符串即可) + * @param ownerSessionId 创建者用户ID(ChatAIID),用于过滤特定创建者的房间 + * @param showPublic 是否显示公有房间,只有明确设置为true时才生效(优先级高于roomType,仅认证用户) + * @param showCreated 是否显示自己创建的房间,只有明确设置为true时才生效(优先级高于roomType,仅认证用户) + * @param showJoined 是否显示自己加入的房间,只有明确设置为true时才生效(优先级高于roomType,仅认证用户) + */ @GET("outside/rooms") - suspend fun getRooms(@Query("page") page: Int = 1, - @Query("pageSize") pageSize: Int = 20, - @Query("isRecommended") isRecommended: Int = 1, - @Query("random") random: Int? = null, + suspend fun getRooms( + @Query("page") page: Int = 1, + @Query("pageSize") pageSize: Int = 20, + @Query("roomId") roomId: Long? = null, + @Query("includeUsers") includeUsers: Boolean? = null, + @Query("isRecommended") isRecommended: Int? = null, + @Query("roomType") roomType: String? = null, + @Query("search") search: String? = null, + @Query("random") random: String? = null, + @Query("ownerSessionId") ownerSessionId: String? = null, + @Query("showPublic") showPublic: Boolean? = null, + @Query("showCreated") showCreated: Boolean? = null, + @Query("showJoined") showJoined: Boolean? = null, ): Response> @GET("outside/rooms/detail") diff --git a/app/src/main/java/com/aiosman/ravenow/entity/Room.kt b/app/src/main/java/com/aiosman/ravenow/entity/Room.kt index e0b9fa1..2c0ddc1 100644 --- a/app/src/main/java/com/aiosman/ravenow/entity/Room.kt +++ b/app/src/main/java/com/aiosman/ravenow/entity/Room.kt @@ -8,6 +8,17 @@ import com.aiosman.ravenow.data.api.ApiClient * 群聊房间 */ +/** + * 房间内的智能体信息实体 + */ +data class PromptTemplateEntity( + val id: Int, + val openId: String, + val title: String, + val desc: String, + val avatar: String +) + data class RoomEntity( val id: Int, val name: String, @@ -21,9 +32,16 @@ data class RoomEntity( val allowInHot: Boolean, val creator: CreatorEntity, val userCount: Int, + val totalMemberCount: Int? = null, val maxMemberLimit: Int, + val maxTotal: Int? = null, + val systemMaxTotal: Int? = null, val canJoin: Boolean, val canJoinCode: Int, + val privateFeePaid: Boolean = false, + val prompts: List = emptyList(), + val createdAt: String? = null, + val updatedAt: String? = null, val users: List, ) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt index 022d360..867a95e 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/AgentViewModel.kt @@ -197,7 +197,7 @@ object AgentViewModel: ViewModel() { page = 1, pageSize = 20, isRecommended = 1, - random = 1 + random = "1" ) if (response.isSuccessful) { val allRooms = response.body()?.list ?: emptyList() diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt index 6ad9c87..4862a65 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/MyProfileViewModel.kt @@ -23,6 +23,10 @@ import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentLoader import com.aiosman.ravenow.entity.MomentLoaderExtraArgs import com.aiosman.ravenow.entity.MomentServiceImpl +import com.aiosman.ravenow.entity.RoomEntity +import com.aiosman.ravenow.data.api.ApiClient +import com.aiosman.ravenow.data.api.RaveNowAPI +import com.aiosman.ravenow.data.Room import com.aiosman.ravenow.event.FollowChangeEvent import com.aiosman.ravenow.event.MomentAddEvent import com.aiosman.ravenow.event.MomentFavouriteChangeEvent @@ -40,6 +44,14 @@ object MyProfileViewModel : ViewModel() { var profile by mutableStateOf(null) var moments by mutableStateOf>(emptyList()) var agents by mutableStateOf>(emptyList()) + var rooms by mutableStateOf>(emptyList()) + var roomsLoading by mutableStateOf(false) + var roomsRefreshing by mutableStateOf(false) + var roomsCurrentPage by mutableStateOf(1) + var roomsHasMore by mutableStateOf(true) + private val roomsPageSize = 20 + private val apiClient: RaveNowAPI = ApiClient.api + val momentLoader: MomentLoader = MomentLoader().apply { pageSize = 20 // 设置与后端一致的页面大小 onListChanged = { @@ -254,4 +266,115 @@ object MyProfileViewModel : ViewModel() { fun onFollowChangeEvent(event: FollowChangeEvent) { momentLoader.updateFollowStatus(event.userId, event.isFollow) } + + /** + * 加载房间列表 + * @param filterType 筛选类型:0=全部,1=公开,2=私有 + * @param pullRefresh 是否下拉刷新 + */ + fun loadRooms(filterType: Int = 0, pullRefresh: Boolean = false) { + // 游客模式下不加载房间列表 + if (AppStore.isGuest) { + Log.d("MyProfileViewModel", "loadRooms: 游客模式下跳过加载房间列表") + return + } + + if (roomsLoading && !pullRefresh) return + + viewModelScope.launch { + try { + roomsLoading = true + roomsRefreshing = pullRefresh + + val currentPage = if (pullRefresh) { + roomsCurrentPage = 1 + roomsHasMore = true + 1 + } else { + roomsCurrentPage + } + + val response = when (filterType) { + 0 -> { + // 全部:显示自己创建或加入的所有房间 + apiClient.getRooms( + page = currentPage, + pageSize = roomsPageSize, + showCreated = true, + showJoined = true + ) + } + 1 -> { + // 公开:显示公开房间中自己创建或加入的 + apiClient.getRooms( + page = currentPage, + pageSize = roomsPageSize, + roomType = "public", + showCreated = true, + showJoined = true + ) + } + 2 -> { + // 私有:显示自己创建或加入的私有房间 + apiClient.getRooms( + page = currentPage, + pageSize = roomsPageSize, + roomType = "private" + ) + } + else -> { + apiClient.getRooms( + page = currentPage, + pageSize = roomsPageSize, + showCreated = true, + showJoined = true + ) + } + } + + if (response.isSuccessful) { + val roomList = response.body()?.list ?: emptyList() + val total = response.body()?.total ?: 0L + + if (pullRefresh || currentPage == 1) { + rooms = roomList.map { it.toRoomtEntity() } + } else { + rooms = rooms + roomList.map { it.toRoomtEntity() } + } + + roomsHasMore = rooms.size < total + if (roomsHasMore && !pullRefresh) { + roomsCurrentPage++ + } + } else { + Log.e("MyProfileViewModel", "loadRooms failed: ${response.code()}") + } + } catch (e: Exception) { + Log.e("MyProfileViewModel", "loadRooms error: ", e) + } finally { + roomsLoading = false + roomsRefreshing = false + } + } + } + + /** + * 加载更多房间 + * @param filterType 筛选类型:0=全部,1=公开,2=私有 + */ + fun loadMoreRooms(filterType: Int = 0) { + if (roomsLoading || !roomsHasMore) return + loadRooms(filterType = filterType, pullRefresh = false) + } + + /** + * 刷新房间列表 + * @param filterType 筛选类型:0=全部,1=公开,2=私有 + */ + fun refreshRooms(filterType: Int = 0) { + rooms = emptyList() + roomsCurrentPage = 1 + roomsHasMore = true + loadRooms(filterType = filterType, pullRefresh = true) + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt index 5e49597..1ce6859 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/composable/GroupChatEmptyContent.kt @@ -14,17 +14,28 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -32,13 +43,46 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.aiosman.ravenow.ConstVars import com.aiosman.ravenow.LocalAppTheme +import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.R +import com.aiosman.ravenow.entity.RoomEntity +import com.aiosman.ravenow.ui.composables.CustomAsyncImage +import com.aiosman.ravenow.ui.composables.rememberDebouncer +import com.aiosman.ravenow.ui.index.tabs.profile.MyProfileViewModel +import com.aiosman.ravenow.ui.modifiers.noRippleClickable +import com.aiosman.ravenow.ui.navigateToGroupChat +import com.aiosman.ravenow.AppStore +@OptIn(ExperimentalMaterialApi::class) @Composable fun GroupChatEmptyContent() { var selectedSegment by remember { mutableStateOf(0) } // 0: 全部, 1: 公开, 2: 私有 val AppColors = LocalAppTheme.current + val context = LocalContext.current + val navController = LocalNavController.current + val viewModel = MyProfileViewModel + + val state = rememberPullRefreshState( + refreshing = viewModel.roomsRefreshing, + onRefresh = { + viewModel.refreshRooms(filterType = selectedSegment) + } + ) + + // 当分段改变时,重新加载数据 + LaunchedEffect(selectedSegment) { + // 切换分段时重新加载 + viewModel.refreshRooms(filterType = selectedSegment) + } + + // 初始加载 + LaunchedEffect(Unit) { + if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) { + viewModel.loadRooms(filterType = selectedSegment) + } + } Column( modifier = Modifier @@ -50,37 +94,192 @@ fun GroupChatEmptyContent() { // 分段控制器 SegmentedControl( selectedIndex = selectedSegment, - onSegmentSelected = { selectedSegment = it }, + onSegmentSelected = { + selectedSegment = it + // LaunchedEffect 会监听 selectedSegment 的变化并自动刷新 + }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) - // 空状态内容(居中) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = Modifier + .fillMaxSize() + .pullRefresh(state) ) { - // 空状态插图 - EmptyStateIllustration() + if (viewModel.rooms.isEmpty() && !viewModel.roomsLoading) { + // 空状态内容(居中) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 空状态插图 + EmptyStateIllustration() + + Spacer(modifier = Modifier.height(9.dp)) + + // 空状态文本 + Text( + text = stringResource(R.string.empty_nothing), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = AppColors.text, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 24.dp), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + itemsIndexed( + items = viewModel.rooms, + key = { _, item -> item.id } + ) { index, room -> + RoomItem( + room = room, + onRoomClick = { roomEntity -> + // 导航到群聊聊天界面 + navController.navigateToGroupChat( + id = roomEntity.trtcRoomId, + name = roomEntity.name, + avatar = roomEntity.avatar + ) + } + ) + + if (index < viewModel.rooms.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 24.dp), + color = AppColors.divider + ) + } + } + + // 加载更多指示器 + if (viewModel.roomsLoading && viewModel.rooms.isNotEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = AppColors.main + ) + } + } + } + + // 加载更多触发 + if (viewModel.roomsHasMore && !viewModel.roomsLoading) { + item { + LaunchedEffect(Unit) { + viewModel.loadMoreRooms(filterType = selectedSegment) + } + } + } + } + } - Spacer(modifier = Modifier.height(9.dp)) - - // 空状态文本 - Text( - text = stringResource(R.string.empty_nothing), - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = AppColors.text, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 24.dp), - maxLines = 2, - overflow = TextOverflow.Ellipsis + PullRefreshIndicator( + refreshing = viewModel.roomsRefreshing, + state = state, + modifier = Modifier.align(Alignment.TopCenter) ) } } } +@Composable +fun RoomItem( + room: RoomEntity, + onRoomClick: (RoomEntity) -> Unit = {} +) { + val AppColors = LocalAppTheme.current + val context = LocalContext.current + val roomDebouncer = rememberDebouncer() + + // 构建头像URL + val avatarUrl = if (room.avatar.isNotEmpty()) { + "${ConstVars.BASE_SERVER}/api/v1/outside/${room.avatar}?token=${AppStore.token}" + } else { + "" + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 12.dp) + .noRippleClickable { + roomDebouncer { + onRoomClick(room) + } + } + ) { + Box { + CustomAsyncImage( + context = context, + imageUrl = avatarUrl, + contentDescription = room.name, + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(12.dp)) + ) + } + + Column( + modifier = Modifier + .weight(1f) + .padding(start = 12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = room.name, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = AppColors.text, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = room.description.ifEmpty { "暂无描述" }, + fontSize = 14.sp, + color = AppColors.secondaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = "${room.userCount}人", + fontSize = 12.sp, + color = AppColors.secondaryText + ) + } + } + } +} + @Composable private fun SegmentedControl( selectedIndex: Int,