From 1a24136c35ae0f92c2f8bf356a915bb9558b8579 Mon Sep 17 00:00:00 2001 From: liudikang <347182558@qq.com> Date: Tue, 23 Sep 2025 11:57:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96AI=E7=95=8C=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=88=86=E9=A1=B5=E5=8A=A0=E8=BD=BD=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E5=8A=A8=E6=80=81=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E6=9B=B4=E5=A4=9A=E6=99=BA=E8=83=BD=E4=BD=93=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=EF=BC=9B=E9=87=8D=E6=9E=84UI=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiosman/ravenow/ui/index/tabs/ai/Agent.kt | 500 +++++++++--------- .../ui/index/tabs/ai/AgentViewModel.kt | 81 ++- 2 files changed, 323 insertions(+), 258 deletions(-) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt index 3e6122b..23e45a2 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/ai/Agent.kt @@ -28,9 +28,14 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -69,13 +74,24 @@ 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.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState 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.foundation.lazy.grid.items as gridItems -@OptIn( ExperimentalFoundationApi::class) +// 检测是否接近列表底部的扩展函数 +fun LazyListState.isScrolledToEnd(buffer: Int = 3): Boolean { + val layoutInfo = this.layoutInfo + val totalItemsCount = layoutInfo.totalItemsCount + val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + + return lastVisibleItemIndex >= (totalItemsCount - buffer) +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun Agent() { val AppColors = LocalAppTheme.current @@ -89,7 +105,6 @@ fun Agent() { var scope = rememberCoroutineScope() val viewModel: AgentViewModel = viewModel() - val scrollState = rememberScrollState() // 确保推荐Agent数据已加载 LaunchedEffect(Unit) { @@ -107,163 +122,92 @@ fun Agent() { } } - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding( - top = statusBarPaddingValues.calculateTopPadding(), - bottom = navigationBarPaddings, - start = 16.dp, - end = 16.dp - ), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row( - modifier = Modifier - .wrapContentHeight() - .height(44.dp) - .fillMaxWidth(), - 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)) - - 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) - ) + val agentItems = viewModel.agentItems + var selectedTabIndex by remember { mutableStateOf(0) } + + // 无限滚动状态 + val listState = rememberLazyListState() + + // 创建一个可观察的滚动到底部状态 + val isScrolledToEnd by remember { + derivedStateOf { + listState.isScrolledToEnd() } - Spacer(modifier = Modifier.height(15.dp)) -// // 搜索框 -// Row( -// modifier = Modifier -// .height(36.dp) -// .weight(1f) -// .clip(shape = RoundedCornerShape(8.dp)) -// .background(AppColors.inputBackground) -// .padding(horizontal = 8.dp, vertical = 0.dp) -// .noRippleClickable { -// // 搜索框点击事件 -// }, -// verticalAlignment = Alignment.CenterVertically -// ) { -// Icon( -// painter = painterResource(id = R.drawable.rider_pro_nav_search), -// contentDescription = null, -// tint = AppColors.inputHint -// ) -// Box { -// Text( -// text = stringResource(R.string.search), -// modifier = Modifier.padding(start = 8.dp), -// color = AppColors.inputHint, -// fontSize = 17.sp -// ) -// } -// } -// Spacer(modifier = Modifier.width(16.dp)) -// // 创建智能体 -// Icon( -// modifier = Modifier -// .size(36.dp) -// .noRippleClickable { -// if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { -// // 检查游客模式,如果是游客则跳转登录 -// if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CREATE_AGENT)) { -// navController.navigate(NavigationRoute.Login.route) -// } else { -// // 导航到添加智能体页面 -// navController.navigate( -// NavigationRoute.AddAgent.route -// ) -// } -// }) { -// lastClickTime = System.currentTimeMillis() -// } -// }, -// painter = painterResource(id = R.drawable.rider_pro_new_post_add_pic), -// contentDescription = null, -// tint = AppColors.text -// ) + } + + // 检测滚动到底部并加载更多数据 + LaunchedEffect(isScrolledToEnd) { + if (isScrolledToEnd && !viewModel.isLoadingMore && agentItems.isNotEmpty() && viewModel.hasMoreData) { + viewModel.loadMoreAgents() + } + } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) + Scaffold( + topBar = { + TopAppBar( + title = { + androidx.compose.material3.Text( + text = "Rave AI", + fontSize = 20.sp, + fontWeight = FontWeight.W900, + color = AppColors.text + ) + }, + actions = { + 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) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = AppColors.background + ) + ) + }, + containerColor = AppColors.background, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { paddingValues -> + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding( + bottom = navigationBarPaddings, + start = 16.dp, + end = 16.dp + ) + ) { - ) { - -// // 标题 -// Row( -// verticalAlignment = Alignment.CenterVertically, -// modifier = Modifier.padding(bottom = 12.dp) -// ) { -// 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_recommend_agent), -// fontSize = 16.sp, -// fontWeight = androidx.compose.ui.text.font.FontWeight.W600, -// color = AppColors.text -// ) -// } - - var selectedTabIndex by remember { mutableStateOf(0) } - - // 标签页 - LazyRow( + // 类别标签页 - 吸顶 + stickyHeader(key = "category_tabs") { + Column( modifier = Modifier .fillMaxWidth() - .wrapContentHeight() - .padding(bottom = 16.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically + .background(AppColors.background) + .padding(top = 4.dp, bottom = 8.dp) ) { - item { - CustomTabItem( - text = stringResource(R.string.agent_recommend), - isSelected = selectedTabIndex == 0, - onClick = { - selectedTabIndex = 0 - viewModel.loadAllAgents() - } - ) - } - - item { - TabSpacer() - } - - // 动态添加分类标签 - viewModel.categories.take(4).forEachIndexed { index, category -> + LazyRow( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { item { CustomTabItem( - text = category.name, - isSelected = selectedTabIndex == index + 1, + text = stringResource(R.string.agent_recommend), + isSelected = selectedTabIndex == 0, onClick = { - selectedTabIndex = index + 1 - viewModel.loadAgentsByCategory(category.id) + selectedTabIndex = 0 + viewModel.loadAllAgents() } ) } @@ -271,117 +215,177 @@ fun Agent() { item { TabSpacer() } - } - item { - CustomTabItem( - text = "scenes", - isSelected = selectedTabIndex == 1, - onClick = { - selectedTabIndex = 1 + // 动态添加分类标签 + viewModel.categories.take(4).forEachIndexed { index, category -> + item { + CustomTabItem( + text = category.name, + isSelected = selectedTabIndex == index + 1, + onClick = { + selectedTabIndex = index + 1 + viewModel.loadAgentsByCategory(category.id) + } + ) } - ) - } - item { - TabSpacer() - } - - item { - CustomTabItem( - text = "voices", - isSelected = selectedTabIndex == 6, - onClick = { - selectedTabIndex = 6 + item { + TabSpacer() } - ) - } + } - item { - TabSpacer() - } + item { + CustomTabItem( + text = "scenes", + isSelected = selectedTabIndex == 1, + onClick = { + selectedTabIndex = 1 + } + ) + } - item { - CustomTabItem( - text = "anime", - isSelected = selectedTabIndex == 7, - onClick = { - selectedTabIndex = 7 - } - ) - } + item { + TabSpacer() + } - item { - TabSpacer() - } + item { + CustomTabItem( + text = "voices", + isSelected = selectedTabIndex == 6, + onClick = { + selectedTabIndex = 6 + } + ) + } - item { - CustomTabItem( - text = "assist", - isSelected = selectedTabIndex == 8, - onClick = { - selectedTabIndex = 8 - } - ) + item { + TabSpacer() + } + + item { + CustomTabItem( + text = "anime", + isSelected = selectedTabIndex == 7, + onClick = { + selectedTabIndex = 7 + } + ) + } + + item { + TabSpacer() + } + + item { + CustomTabItem( + text = "assist", + isSelected = selectedTabIndex == 8, + onClick = { + selectedTabIndex = 8 + } + ) + } } } - - when { - selectedTabIndex == 0 -> { - AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) - } - selectedTabIndex in 1..viewModel.categories.size -> { - AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) - } - else -> { - val shuffledAgents = viewModel.agentItems.shuffled().take(15) - AgentViewPagerSection(agentItems = shuffledAgents, viewModel) + } + // 推荐内容区域 + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + ) { + when { + selectedTabIndex == 0 -> { + AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) + } + selectedTabIndex in 1..viewModel.categories.size -> { + AgentViewPagerSection(agentItems = viewModel.agentItems.take(15), viewModel) + } + else -> { + val shuffledAgents = viewModel.agentItems.shuffled().take(15) + AgentViewPagerSection(agentItems = shuffledAgents, viewModel) + } } } } - 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 - ) - - } - Spacer(modifier = Modifier.height(16.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - val agentItems = viewModel.agentItems - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(32.dp) - ) { - items(agentItems) { agentItem -> - AgentCardSquare( - agentItem = agentItem, - viewModel = viewModel, - navController = LocalNavController.current + // "发现更多" 标题 - 吸顶 + stickyHeader(key = "discover_more") { + Row( + modifier = Modifier + .fillMaxWidth() + .background(AppColors.background) + .padding(top = 8.dp, bottom = 12.dp), + 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 + ) + } + } + + // Agent网格 - 使用行式布局 + items( + items = agentItems.chunked(2), + key = { row -> row.firstOrNull()?.openId ?: "" } + ) { rowItems -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + rowItems.forEach { agentItem -> + Box( + modifier = Modifier.weight(1f) + ) { + AgentCardSquare( + agentItem = agentItem, + viewModel = viewModel, + navController = LocalNavController.current + ) + } + } + // 如果这一行只有一个item,添加一个空的占位符 + if (rowItems.size == 1) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + + // 加载更多指示器 + if (viewModel.isLoadingMore) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + horizontalArrangement = Arrangement.Center + ) { + androidx.compose.material3.CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = AppColors.text, + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(12.dp)) + androidx.compose.material3.Text( + text = "加载中...", + color = AppColors.secondaryText, + fontSize = 14.sp + ) + } } } } 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 82b53e9..cc05705 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 @@ -38,42 +38,82 @@ object AgentViewModel: ViewModel() { var isLoading by mutableStateOf(false) private set + // 分页相关状态 + var isLoadingMore by mutableStateOf(false) + private set + + var currentPage by mutableStateOf(1) + private set + + var hasMoreData by mutableStateOf(true) + private set + + private val pageSize = 20 + private var currentCategoryId: Int? = null + init { loadAgentData() loadCategories() } - private fun loadAgentData(categoryId: Int? = null) { + private fun loadAgentData(categoryId: Int? = null, page: Int = 1, isLoadMore: Boolean = false) { viewModelScope.launch { - isLoading = true + if (isLoadMore) { + isLoadingMore = true + } else { + isLoading = true + // 重置分页状态 + currentPage = 1 + hasMoreData = true + currentCategoryId = categoryId + } + errorMessage = null try { val response = if (categoryId != null) { // 根据分类ID获取智能体 apiClient.getAgent( - page = 1, - pageSize = 20, + page = page, + pageSize = pageSize, withWorkflow = 1, categoryIds = listOf(categoryId) ) } else { // 获取所有智能体 - apiClient.getAgent(page = 1, pageSize = 20, withWorkflow = 1) + apiClient.getAgent(page = page, pageSize = pageSize, withWorkflow = 1) } if (response.isSuccessful) { - val agents = response.body()?.data?.list ?: emptyList() - - agentItems = agents.map { agent -> + val responseData = response.body()?.data + val agents = responseData?.list ?: emptyList() + val newAgentItems = agents.map { agent -> AgentItem.fromAgent(agent) } + + if (isLoadMore) { + // 加载更多:追加到现有列表 + agentItems = agentItems + newAgentItems + currentPage = page + } else { + // 首次加载或刷新:替换整个列表 + agentItems = newAgentItems + currentPage = 1 + } + + // 检查是否还有更多数据 + hasMoreData = agents.size >= pageSize + } else { errorMessage = "获取Agent数据失败: ${response.code()}" } } catch (e: Exception) { errorMessage = "网络请求失败: ${e.message}" } finally { - isLoading = false + if (isLoadMore) { + isLoadingMore = false + } else { + isLoading = false + } } } } @@ -115,6 +155,23 @@ object AgentViewModel: ViewModel() { fun loadAllAgents() { loadAgentData() } + + /** + * 加载更多Agent数据 + */ + fun loadMoreAgents() { + // 检查是否正在加载或没有更多数据 + if (isLoadingMore || !hasMoreData) { + return + } + + val nextPage = currentPage + 1 + loadAgentData( + categoryId = currentCategoryId, + page = nextPage, + isLoadMore = true + ) + } fun createSingleChat( openId: String, ) { @@ -176,6 +233,10 @@ object AgentViewModel: ViewModel() { errorMessage = null isRefreshing = false isLoading = false + isLoadingMore = false + currentPage = 1 + hasMoreData = true + currentCategoryId = null } } @@ -197,4 +258,4 @@ data class CategoryItem( ) } } -} \ No newline at end of file +}