Refactor: Optimize Agent tab UI and add chat room recommendations

- Implemented lazy loading for Agent list with pagination.
- Added a section for recommended chat rooms.
- Restructured Agent tab UI:
    - Fixed the top search bar.
    - Organized content into scrollable sections for "Today's Picks", "Recommended Chat Rooms", and "Find Agents".
    - Improved Agent card design with theme-aware backgrounds.
- Introduced `ChatRoom` data class and integrated chat room loading logic in `AgentViewModel`.
- Updated `RiderProAPI` to include `random` parameter for fetching random rooms.
- Enhanced Agent card and chat room card click handling with debounce.
This commit is contained in:
2025-09-15 23:30:10 +08:00
parent a1196715d0
commit 0e0d622864
3 changed files with 390 additions and 93 deletions

View File

@@ -619,6 +619,7 @@ interface RaveNowAPI {
suspend fun getRooms(@Query("page") page: Int = 1,
@Query("pageSize") pageSize: Int = 20,
@Query("isRecommended") isRecommended: Int = 1,
@Query("random") random: Int? = null
): Response<ListContainer<Room>>
@GET("outside/rooms/detail")

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -36,6 +37,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -69,13 +71,11 @@ import com.aiosman.ravenow.utils.DebounceUtils
import com.aiosman.ravenow.utils.ResourceCleanupManager
import kotlinx.coroutines.launch
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.ui.zIndex
@OptIn( ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Agent() {
val AppColors = LocalAppTheme.current
@@ -95,10 +95,22 @@ fun Agent() {
LaunchedEffect(Unit) {
viewModel.ensureDataLoaded()
}
// 监听滚动状态,实现自动加载更多
LaunchedEffect(scrollState) {
snapshotFlow { scrollState.value }
.collect { scrollValue ->
val maxScroll = scrollState.maxValue
if (scrollValue >= maxScroll - 100 && !viewModel.isLoading) {
// 滚动到接近底部时加载更多
viewModel.loadMoreAgentData()
}
}
}
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
// 页面退出时只清理必要的资源不清理推荐Agent数据
DisposableEffect(Unit) {
onDispose {
@@ -107,49 +119,61 @@ fun Agent() {
}
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(
top = statusBarPaddingValues.calculateTopPadding(),
bottom = navigationBarPaddings,
start = 16.dp,
end = 16.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
Box(
modifier = Modifier.fillMaxSize()
) {
Row(
// 固定顶部搜索条
Box(
modifier = Modifier
.wrapContentHeight()
.height(44.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
.fillMaxWidth()
.background(AppColors.background)
.zIndex(999.0f)
.height(44.dp + statusBarPaddingValues.calculateTopPadding())
) {
androidx.compose.material3.Text(
text = "Rave AI",
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
Row(
modifier = Modifier
.align(Alignment.CenterVertically)
)
.fillMaxWidth()
.fillMaxHeight().padding(top = 32.dp, start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
androidx.compose.material3.Text(
text = "Rave AI",
fontSize = 20.sp,
fontWeight = FontWeight.W900,
color = AppColors.text,
modifier = Modifier
.align(Alignment.CenterVertically)
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
colorFilter = ColorFilter.tint(AppColors.text)
)
Image(
painter = painterResource(id = R.drawable.rider_pro_nav_search),
contentDescription = "search",
modifier = Modifier
.size(24.dp)
.noRippleClickable {
navController.navigate(NavigationRoute.Search.route)
},
colorFilter = ColorFilter.tint(AppColors.text)
)
}
}
Spacer(modifier = Modifier.height(15.dp))
// 可滚动的内容区域
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(
top = 44.dp + statusBarPaddingValues.calculateTopPadding() + 15.dp,
bottom = navigationBarPaddings,
start = 16.dp,
end = 16.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// // 搜索框
// Row(
// modifier = Modifier
@@ -311,6 +335,7 @@ fun Agent() {
0 -> {
AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel)
}
else -> {
val shuffledAgents = viewModel.agentItems.shuffled().take(15)
AgentViewPagerSection(agentItems = shuffledAgents, viewModel)
@@ -318,56 +343,106 @@ fun Agent() {
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
Image(
painter = painterResource(R.mipmap.rider_pro_agent2),
contentDescription = "agent",
modifier = Modifier.size(28.dp),
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
// 推荐聊天房间
ChatRoomsSection(
chatRooms = viewModel.chatRooms,
navController = LocalNavController.current
)
Spacer(modifier = Modifier.height(20.dp))
}
Spacer(modifier = Modifier.height(50.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
val agentItems = viewModel.agentItems.take(15)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(50.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
// center the tabs
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Bottom
) {
items(agentItems) { agentItem ->
AgentCardSquare(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
Image(
painter = painterResource(R.mipmap.rider_pro_agent2),
contentDescription = "agent",
modifier = Modifier.size(28.dp),
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = stringResource(R.string.agent_find),
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
}
Spacer(modifier = Modifier.height(20.dp))
// Agent两列网格布局
AgentGridLayout(
agentItems = viewModel.agentItems,
viewModel = viewModel,
navController = LocalNavController.current
)
}
}
}
@Composable
fun AgentGridLayout(
agentItems: List<AgentItem>,
viewModel: AgentViewModel,
navController: NavHostController
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
// 将agentItems按两列分组
agentItems.chunked(2).forEachIndexed { rowIndex, rowItems ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (rowIndex == 0) 30.dp else 20.dp, // 第一行添加更多顶部间距
bottom = 20.dp
),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一列
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[0],
viewModel = viewModel,
navController = navController
)
}
// 第二列(如果存在)
if (rowItems.size > 1) {
Box(
modifier = Modifier.weight(1f)
) {
AgentCardSquare(
agentItem = rowItems[1],
viewModel = viewModel,
navController = navController
)
}
} else {
// 如果只有一列,添加空白占位
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navController: NavHostController) {
fun AgentCardSquare(
agentItem: AgentItem,
viewModel: AgentViewModel,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current
val cardHeight = 180.dp
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
@@ -379,7 +454,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
modifier = Modifier
.fillMaxWidth()
.height(cardHeight)
.background(Color(0xFFE0E0E0), RoundedCornerShape(12.dp)) // 灰色背景
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp)) // 主题背景
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
@@ -394,7 +469,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
modifier = Modifier
.offset(y = -avatarSize / 2)
.size(avatarSize)
.background(Color.White, RoundedCornerShape(avatarSize / 2))
.background(AppColors.background, RoundedCornerShape(avatarSize / 2))
.clip(RoundedCornerShape(avatarSize / 2)),
contentAlignment = Alignment.Center
) {
@@ -452,7 +527,7 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
.width(80.dp)
.height(32.dp)
.background(
color = Color(0X147c7480),
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.clickable {
@@ -483,9 +558,10 @@ fun AgentCardSquare(agentItem: AgentItem, viewModel: AgentViewModel, navControll
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel) {
fun AgentViewPagerSection(agentItems: List<AgentItem>, viewModel: AgentViewModel) {
val AppColors = LocalAppTheme.current
// 每页显示5个agent
@@ -558,7 +634,13 @@ fun AgentViewPagerSection(agentItems: List<AgentItem>,viewModel: AgentViewModel)
}
@Composable
fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int, modifier: Modifier = Modifier,navController: NavHostController) {
fun AgentPage(
viewModel: AgentViewModel,
agentItems: List<AgentItem>,
page: Int,
modifier: Modifier = Modifier,
navController: NavHostController
) {
Column(
modifier = modifier
.fillMaxSize()
@@ -566,7 +648,11 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
) {
// 显示3个agent
agentItems.forEachIndexed { index, agentItem ->
AgentCard2(agentItem = agentItem, viewModel = viewModel, navController = LocalNavController.current)
AgentCard2(
agentItem = agentItem,
viewModel = viewModel,
navController = LocalNavController.current
)
if (index < agentItems.size - 1) {
Spacer(modifier = Modifier.height(8.dp))
}
@@ -576,9 +662,9 @@ fun AgentPage(viewModel: AgentViewModel,agentItems: List<AgentItem>, page: Int,
@SuppressLint("SuspiciousIndentation")
@Composable
fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: NavHostController) {
fun AgentCard2(viewModel: AgentViewModel, agentItem: AgentItem, navController: NavHostController) {
val AppColors = LocalAppTheme.current
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
@@ -592,7 +678,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
Box(
modifier = Modifier
.size(48.dp)
.background(Color(0xFFF5F5F5), RoundedCornerShape(24.dp))
.background(AppColors.secondaryBackground, RoundedCornerShape(24.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController)
@@ -656,7 +742,7 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
modifier = Modifier
.size(width = 60.dp, height = 32.dp)
.background(
color = Color(0X147c7480),
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.clickable {
@@ -686,3 +772,132 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
}
}
}
@Composable
fun ChatRoomsSection(
chatRooms: List<ChatRoom>,
navController: NavHostController
) {
val AppColors = LocalAppTheme.current
Column(
modifier = Modifier.fillMaxWidth()
) {
// 标题
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.mipmap.rider_pro_agent2),
contentDescription = "chat room",
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(4.dp))
androidx.compose.material3.Text(
text = "推荐聊天房间",
fontSize = 16.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600,
color = AppColors.text
)
}
// 3行宫格布局
Column(
modifier = Modifier.fillMaxWidth()
) {
// 将聊天房间按3个一组分组
chatRooms.chunked(3).forEach { rowRooms ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
rowRooms.forEach { chatRoom ->
ChatRoomCard(
chatRoom = chatRoom,
navController = navController,
modifier = Modifier.weight(1f)
)
}
// 如果这一行不足3个添加空白占位
repeat(3 - rowRooms.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
}
@Composable
fun ChatRoomCard(
chatRoom: ChatRoom,
navController: NavHostController,
modifier: Modifier = Modifier
) {
val AppColors = LocalAppTheme.current
val cardSize = 100.dp
// 防抖状态
var lastClickTime by remember { mutableStateOf(0L) }
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
// 正方形卡片
Box(
modifier = Modifier
.size(cardSize)
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp))
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 这里可以添加进入聊天房间的逻辑
// navController.navigate(NavigationRoute.ChatRoom.route.replace("{id}", chatRoom.id))
}) {
lastClickTime = System.currentTimeMillis()
}
},
contentAlignment = Alignment.Center
) {
// 优先显示banner如果没有banner则显示头像
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
if (imageUrl.isNotEmpty()) {
CustomAsyncImage(
imageUrl = imageUrl,
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
modifier = Modifier
.size(cardSize)
.clip(RoundedCornerShape(12.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop
)
} else {
// 默认房间图标
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "默认房间图标",
modifier = Modifier.size(cardSize * 0.4f),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// 房间名称
androidx.compose.material3.Text(
text = chatRoom.name,
fontSize = 12.sp,
color = AppColors.text,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}

View File

@@ -6,6 +6,10 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.aiosman.ravenow.AppState
import com.aiosman.ravenow.AppStore
import com.aiosman.ravenow.ConstVars
import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.api.SingleChatRequestBody
@@ -15,6 +19,14 @@ import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userServic
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre.AgentItem
import kotlinx.coroutines.launch
data class ChatRoom(
val id: String,
val name: String,
val avatar: String = "",
val banner: String = "",
val memberCount: Int = 0
)
object AgentViewModel: ViewModel() {
private val apiClient: RaveNowAPI = ApiClient.api
@@ -22,32 +34,43 @@ object AgentViewModel: ViewModel() {
var agentItems by mutableStateOf<List<AgentItem>>(emptyList())
private set
var chatRooms by mutableStateOf<List<ChatRoom>>(emptyList())
private set
var rooms by mutableStateOf<List<Room>>(emptyList())
private set
var errorMessage by mutableStateOf<String?>(null)
private set
var isRefreshing by mutableStateOf(false)
private set
var isLoading by mutableStateOf(false)
private set
private var currentPage = 1
private var hasMoreData = true
init {
loadAgentData()
loadChatRooms()
}
private fun loadAgentData() {
viewModelScope.launch {
isLoading = true
errorMessage = null
currentPage = 1
hasMoreData = true
try {
val response = apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1)
val response = apiClient.getAgent(page = currentPage, pageSize = 20, withWorkflow = 1)
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
agentItems = agents.map { agent ->
AgentItem.fromAgent(agent)
}
hasMoreData = agents.size >= 20
} else {
errorMessage = "获取Agent数据失败: ${response.code()}"
}
@@ -58,6 +81,62 @@ object AgentViewModel: ViewModel() {
}
}
}
private fun loadChatRooms() {
viewModelScope.launch {
try {
val response = apiClient.getRooms(page = 1, pageSize = 20, isRecommended = 1, random = 1)
if (response.isSuccessful) {
rooms = response.body()?.list ?: emptyList()
// 转换为ChatRoom格式用于兼容现有UI
chatRooms = rooms.map { room ->
ChatRoom(
id = room.trtcRoomId,
name = room.name,
avatar = room.avatar,
banner = ConstVars.BASE_SERVER + "/api/v1/outside/" + room.recommendBanner + "?token=${AppStore.token}",
memberCount = room.userCount
)
}
} else {
}
} catch (e: Exception) {
// 如果网络请求失败,使用默认数据
}
}
}
/**
* 加载更多Agent数据
*/
fun loadMoreAgentData() {
if (!hasMoreData || isLoading) return
viewModelScope.launch {
isLoading = true
try {
val nextPage = currentPage + 1
val response = apiClient.getAgent(page = nextPage, pageSize = 20, withWorkflow = 1)
if (response.isSuccessful) {
val agents = response.body()?.data?.list ?: emptyList()
val newAgentItems = agents.map { agent ->
AgentItem.fromAgent(agent)
}
agentItems = agentItems + newAgentItems
currentPage = nextPage
hasMoreData = agents.size >= 20
} else {
errorMessage = "加载更多Agent数据失败: ${response.code()}"
}
} catch (e: Exception) {
errorMessage = "网络请求失败: ${e.message}"
} finally {
isLoading = false
}
}
}
fun createSingleChat(
openId: String,
) {
@@ -119,6 +198,8 @@ object AgentViewModel: ViewModel() {
errorMessage = null
isRefreshing = false
isLoading = false
currentPage = 1
hasMoreData = true
}
}