This commit is contained in:
weber
2025-08-25 18:35:06 +08:00
parent 77033854f0
commit df75c710e5
20 changed files with 353 additions and 181 deletions

View File

@@ -41,7 +41,7 @@ data class AgentMomentRequestBody(
data class SingleChatRequestBody( data class SingleChatRequestBody(
@SerializedName("agentOpenId") @SerializedName("agentOpenId")
val generateText: String? = null, val agentOpenId: String? = null,
@SerializedName("agentTrtcId") @SerializedName("agentTrtcId")
val agentTrtcId: String? = null, val agentTrtcId: String? = null,
) )

View File

@@ -4,8 +4,10 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import com.aiosman.ravenow.data.ListContainer import com.aiosman.ravenow.data.ListContainer
import com.aiosman.ravenow.data.AgentService import com.aiosman.ravenow.data.AgentService
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.UploadImage import com.aiosman.ravenow.data.UploadImage
import com.aiosman.ravenow.data.UserService
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@@ -47,6 +49,79 @@ suspend fun createAgent(
} }
/**
* 智能体信息分页加载器
*/
class AgentPagingSource(
private val agentRemoteDataSource: AgentRemoteDataSource,
) : PagingSource<Int, AgentEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AgentEntity> {
return try {
val currentPage = params.key ?: 1
val users = agentRemoteDataSource.getAgent(
pageNumber = currentPage
)
LoadResult.Page(
data = users.list,
prevKey = if (currentPage == 1) null else currentPage - 1,
nextKey = if (users.list.isEmpty()) null else users.page + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, AgentEntity>): Int? {
return state.anchorPosition
}
}
class AgentRemoteDataSource(
private val agentService: AgentService,
) {
suspend fun getAgent(
pageNumber: Int,
): ListContainer<AgentEntity> {
return agentService.getAgent(
pageNumber = pageNumber
)
}
}
class AgentServiceImpl() : AgentService {
val agentBackend = AgentBackend()
override suspend fun getAgent(pageNumber: Int, pageSize: Int): ListContainer<AgentEntity> {
return agentBackend.getAgent(
pageNumber = pageNumber,
)
}
}
class AgentBackend {
val DataBatchSize = 20
suspend fun getAgent(
pageNumber: Int,
): ListContainer<AgentEntity> {
val resp = ApiClient.api.getMyAgent(
pageSize = DataBatchSize,
page = pageNumber,
)
val body = resp.body() ?: throw ServiceException("Failed to get moments")
return ListContainer(
total = body.total,
page = pageNumber,
pageSize = DataBatchSize,
list = body.list.map { it.toAgentEntity() }
)
}
}
data class AgentEntity( data class AgentEntity(
//val author: String, //val author: String,
val avatar: String, val avatar: String,

View File

@@ -534,7 +534,7 @@ fun NavHostController.navigateToGroupInfo(id: String) {
val encodedId = java.net.URLEncoder.encode(id, "UTF-8") val encodedId = java.net.URLEncoder.encode(id, "UTF-8")
navigate( navigate(
route = NavigationRoute.ChatGroup.route route = NavigationRoute.GroupInfo.route
.replace("{id}", encodedId) .replace("{id}", encodedId)
) )

View File

@@ -58,9 +58,7 @@ object AddAgentViewModel : ViewModel() {
) )
println("AddAgentViewModel: Agent created successfully with ID: ${result.id}") println("AddAgentViewModel: Agent created successfully with ID: ${result.id}")
// 通知相关ViewModel更新列表
notifyAgentCreated(result)
return result return result
} catch (e: Exception) { } catch (e: Exception) {
@@ -71,10 +69,7 @@ object AddAgentViewModel : ViewModel() {
} }
} }
private fun notifyAgentCreated(agent: AgentEntity) {
// 通知我的智能体列表更新
com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.addAgentToList(agent)
}
fun validate(): String? { fun validate(): String? {
return when { return when {

View File

@@ -199,9 +199,11 @@ fun ChatAiScreen(userId: String) {
color = AppColors.text, color = AppColors.text,
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W700 fontWeight = androidx.compose.ui.text.font.FontWeight.W700
) ),
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.width(8.dp))
Box { Box {
Image( Image(
painter = painterResource(R.drawable.rider_pro_more_horizon), painter = painterResource(R.drawable.rider_pro_more_horizon),

View File

@@ -199,6 +199,7 @@ fun ChatScreen(userId: String) {
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
, ,
maxLines = 1, maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
style = TextStyle( style = TextStyle(
color = AppColors.text, color = AppColors.text,
fontSize = 18.sp, fontSize = 18.sp,

View File

@@ -206,7 +206,7 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
fontWeight = androidx.compose.ui.text.font.FontWeight.W700 fontWeight = androidx.compose.ui.text.font.FontWeight.W700
), ),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow =TextOverflow.Ellipsis,
) )
} }
@@ -220,11 +220,13 @@ fun GroupChatScreen(groupId: String,name: String,avatar: String,) {
color = AppColors.text, color = AppColors.text,
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
) ),
maxLines = 1,
overflow =TextOverflow.Ellipsis,
) )
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.width(8.dp))
Box { Box {
Image( Image(
painter = painterResource(R.drawable.rider_pro_more_horizon), painter = painterResource(R.drawable.rider_pro_more_horizon),

View File

@@ -59,16 +59,15 @@ fun AgentCard(
Row( Row(
modifier = Modifier modifier = Modifier
) { ) {
// 使用remember基于agentEntity.id来缓存图片避免滑动时重复加载
Box( Box(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.clip(RoundedCornerShape(40.dp)) .clip(RoundedCornerShape(40.dp))
) { ) {
CustomAsyncImage( CustomAsyncImage(
LocalContext.current, context,
agentEntity.avatar, agentEntity.avatar,
contentDescription = "", contentDescription = agentEntity.openId,
modifier = Modifier.size(40.dp), modifier = Modifier.size(40.dp),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )

View File

@@ -23,6 +23,28 @@ import com.aiosman.ravenow.utils.Utils.getImageLoader
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/**
* CustomAsyncImage 组件使用说明:
*
* @param context 上下文,可选
* @param imageUrl 图片URL或Bitmap对象
* @param contentDescription 图片描述
* @param modifier 修饰符
* @param blurHash 模糊哈希值(暂未使用)
* @param placeholderRes 加载时显示的占位符图片资源ID
* @param errorRes 加载失败时显示的错误图片资源ID
* @param defaultRes 当imageUrl为空或null时显示的默认图片资源ID优先级最高
* @param contentScale 图片缩放模式
*
* 使用示例:
* CustomAsyncImage(
* imageUrl = "https://example.com/image.jpg",
* contentDescription = "用户头像",
* defaultRes = R.mipmap.default_avatar,
* placeholderRes = R.mipmap.loading_placeholder,
* errorRes = R.mipmap.error_image
* )
*/
@Composable @Composable
fun rememberImageBitmap(imageUrl: String, imageLoader: ImageLoader): Bitmap? { fun rememberImageBitmap(imageUrl: String, imageLoader: ImageLoader): Bitmap? {
val context = LocalContext.current val context = LocalContext.current
@@ -53,6 +75,10 @@ fun CustomAsyncImage(
blurHash: String? = null, blurHash: String? = null,
@DrawableRes @DrawableRes
placeholderRes: Int? = null, placeholderRes: Int? = null,
@DrawableRes
errorRes: Int? = null,
@DrawableRes
defaultRes: Int? = null,
contentScale: ContentScale = ContentScale.Crop contentScale: ContentScale = ContentScale.Crop
) { ) {
val localContext = LocalContext.current val localContext = LocalContext.current
@@ -62,10 +88,11 @@ fun CustomAsyncImage(
// 处理 imageUrl 为 null 或空字符串的情况 // 处理 imageUrl 为 null 或空字符串的情况
if (imageUrl == null || imageUrl == "") { if (imageUrl == null || imageUrl == "") {
// 如果 imageUrl 为 null 且有占位符,则直接显示占位符 // 优先使用 defaultRes然后是 placeholderRes
if (placeholderRes != null) { val fallbackRes = defaultRes ?: placeholderRes
if (fallbackRes != null) {
Image( Image(
painter = androidx.compose.ui.res.painterResource(placeholderRes), painter = androidx.compose.ui.res.painterResource(fallbackRes),
contentDescription = contentDescription, contentDescription = contentDescription,
modifier = modifier, modifier = modifier,
contentScale = contentScale contentScale = contentScale
@@ -97,6 +124,10 @@ fun CustomAsyncImage(
if (placeholderRes != null) { if (placeholderRes != null) {
placeholder(placeholderRes) placeholder(placeholderRes)
} }
// 设置错误时显示的图片
if (errorRes != null) {
error(errorRes)
}
} }
.build(), .build(),
contentDescription = contentDescription, contentDescription = contentDescription,
@@ -104,4 +135,38 @@ fun CustomAsyncImage(
contentScale = contentScale, contentScale = contentScale,
imageLoader = imageLoader imageLoader = imageLoader
) )
} }
/*
使用示例:
1. 基本使用(带默认图片):
CustomAsyncImage(
imageUrl = user.avatar,
contentDescription = "用户头像",
defaultRes = R.mipmap.default_avatar
)
2. 完整配置:
CustomAsyncImage(
imageUrl = "https://example.com/image.jpg",
contentDescription = "产品图片",
defaultRes = R.mipmap.default_product,
placeholderRes = R.mipmap.loading_placeholder,
errorRes = R.mipmap.error_image,
contentScale = ContentScale.Crop
)
3. 处理空URL
CustomAsyncImage(
imageUrl = "", // 空字符串会显示默认图片
contentDescription = "头像",
defaultRes = R.mipmap.default_avatar
)
4. 处理Bitmap
CustomAsyncImage(
imageUrl = bitmapObject, // Bitmap对象会直接显示
contentDescription = "裁剪后的图片"
)
*/

View File

@@ -187,11 +187,11 @@ fun GroupChatInfoScreen(groupId: String) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.noRippleClickable { modifier = Modifier.noRippleClickable {
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
if (viewModel.notificationStrategy == "mute") { /*if (viewModel.notificationStrategy == "mute") {
viewModel.updateNotificationStrategy("active") viewModel.updateNotificationStrategy("active")
} else { } else {
viewModel.updateNotificationStrategy("mute") viewModel.updateNotificationStrategy("mute")
} }*/
} }
} }
) { ) {

View File

@@ -61,7 +61,7 @@ object AgentViewModel: ViewModel() {
openId: String, openId: String,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(generateText = openId)) val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
} }
} }

View File

@@ -114,7 +114,7 @@ object HotAgentViewModel : ViewModel() {
openId: String, openId: String,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(generateText = openId)) val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
} }
} }

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
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.paging.compose.collectAsLazyPagingItems
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
@@ -39,13 +40,17 @@ fun MineAgent() {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val navController = LocalNavController.current val navController = LocalNavController.current
val model = MineAgentViewModel val model = MineAgentViewModel
var agentList = model.agentList
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state = rememberPullRefreshState(model.refreshing, onRefresh = { val state = rememberPullRefreshState(model.refreshing, onRefresh = {
model.refreshPager(pullRefresh = true) model.refreshPager(
//pullRefresh = true
)
}) })
val listState = rememberLazyListState() val listState = rememberLazyListState()
var dataFlow = model.agentList
var agentList = dataFlow.collectAsLazyPagingItems()
// observe list scrolling // observe list scrolling
val reachedBottom by remember { val reachedBottom by remember {
derivedStateOf { derivedStateOf {
@@ -56,33 +61,19 @@ fun MineAgent() {
// load more if scrolled to bottom // load more if scrolled to bottom
LaunchedEffect(reachedBottom) { LaunchedEffect(reachedBottom) {
if (reachedBottom && !model.isLoading && model.hasNext) { if (reachedBottom) {
model.loadMore()
}
}
// 只在首次加载时刷新避免从AddAgent返回时重复刷新
LaunchedEffect(Unit) {
if (model.agentList.isEmpty() && !model.isLoading) {
model.refreshPager() model.refreshPager()
} }
} }
LaunchedEffect(Unit) {
val context = LocalContext.current model.refreshPager()
// 当智能体列表加载完成后,预加载图片
LaunchedEffect(agentList) {
if (agentList.isNotEmpty()) {
model.preloadImages(context)
}
} }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
if(agentList.isEmpty() && !model.isLoading) { if(agentList.itemCount == 0 && !model.isLoading) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
@@ -119,23 +110,21 @@ fun MineAgent() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState state = listState
) { ) {
items( items(agentList.itemCount) { index ->
agentList.size, agentList[index]?.let { agent ->
key = { idx -> agentList[idx].id } // 使用智能体ID作为key避免重新创建 AgentCard(
) { idx -> agentEntity = agent,
val agentItem = agentList[idx] onClick = {
AgentCard( model.createSingleChat(agent.openId)
agentEntity = agentItem, model.goToChatAi(agent.openId, navController)
onClick = { },
model.createSingleChat(agentItem.openId)
model.goToChatAi(agentItem.openId,navController)
},
) )
}
} }
// 加载更多指示器 // 加载更多指示器
if (model.isLoading && agentList.isNotEmpty()) { if (model.isLoading && agentList.itemCount != 0) {
item { item {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -6,19 +6,42 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.AgentService
import com.aiosman.ravenow.data.MomentService
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.ServiceException import com.aiosman.ravenow.data.ServiceException
import com.aiosman.ravenow.data.api.SingleChatRequestBody import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.AgentPagingSource
import com.aiosman.ravenow.entity.AgentRemoteDataSource
import com.aiosman.ravenow.entity.AgentServiceImpl
import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.entity.MomentPagingSource
import com.aiosman.ravenow.entity.MomentRemoteDataSource
import com.aiosman.ravenow.entity.MomentServiceImpl
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot.HotMomentViewModel.firstLoad
import com.aiosman.ravenow.ui.navigateToChatAi import com.aiosman.ravenow.ui.navigateToChatAi
import com.tencent.imsdk.v2.V2TIMConversationOperationResult import com.tencent.imsdk.v2.V2TIMConversationOperationResult
import com.tencent.imsdk.v2.V2TIMManager import com.tencent.imsdk.v2.V2TIMManager
import com.tencent.imsdk.v2.V2TIMValueCallback import com.tencent.imsdk.v2.V2TIMValueCallback
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
object MineAgentViewModel : ViewModel() { object MineAgentViewModel : ViewModel() {
var agentList by mutableStateOf<List<AgentEntity>>(emptyList()) private val agentService: AgentService = AgentServiceImpl()
private val _agentList =
MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty())
val agentList = _agentList.asStateFlow()
var refreshing by mutableStateOf(false) var refreshing by mutableStateOf(false)
var isLoading by mutableStateOf(false) var isLoading by mutableStateOf(false)
var hasNext by mutableStateOf(true) var hasNext by mutableStateOf(true)
@@ -29,87 +52,25 @@ object MineAgentViewModel : ViewModel() {
private val preloadedImageIds = mutableSetOf<Int>() private val preloadedImageIds = mutableSetOf<Int>()
private val pageSize = 20 private val pageSize = 20
init { fun refreshPager() {
// 延迟初始化,避免在页面切换时立即加载 if (!firstLoad) {
// refreshPager() return
}
fun refreshPager(pullRefresh: Boolean = false) {
if (isLoading && !pullRefresh) return
viewModelScope.launch {
try {
isLoading = true
refreshing = pullRefresh
error = null
// 清除预加载记录,强制重新加载图片
if (pullRefresh) {
clearPreloadedImages()
}
val response = ApiClient.api.getMyAgent(
page = 1,
pageSize = pageSize
)
val body = response.body()
if (body != null) {
val newAgents = body.list.map { it.toAgentEntity() }
// 只有在列表为空或者是下拉刷新时才替换整个列表
if (agentList.isEmpty() || pullRefresh) {
agentList = newAgents
} else {
// 否则只添加新的智能体
val existingIds = agentList.map { it.id }.toSet()
val newAgentsToAdd = newAgents.filter { it.id !in existingIds }
if (newAgentsToAdd.isNotEmpty()) {
agentList = agentList + newAgentsToAdd
}
}
currentPage = 1
hasNext = newAgents.size == pageSize
} else {
throw ServiceException("Failed to load agents")
}
} catch (e: Exception) {
error = e.message ?: "加载失败"
e.printStackTrace()
} finally {
isLoading = false
refreshing = false
}
} }
} firstLoad = false
fun loadMore() {
if (isLoading || !hasNext) return
viewModelScope.launch { viewModelScope.launch {
try { Pager(
isLoading = true config = PagingConfig(pageSize = 5, enablePlaceholders = false),
error = null pagingSourceFactory = {
AgentPagingSource(
val response = ApiClient.api.getMyAgent(
page = currentPage + 1, AgentRemoteDataSource(agentService),
pageSize = pageSize //trend = true
) )
val body = response.body()
if (body != null) {
val newAgents = body.list.map { it.toAgentEntity() }
agentList = agentList + newAgents
currentPage += 1
hasNext = newAgents.size == pageSize
} else {
throw ServiceException("Failed to load more agents")
} }
} catch (e: Exception) { ).flow.cachedIn(viewModelScope).collectLatest {
error = e.message ?: "加载更多失败" _agentList.value = it
e.printStackTrace()
} finally {
isLoading = false
} }
} }
} }
@@ -144,7 +105,7 @@ object MineAgentViewModel : ViewModel() {
openId: String, openId: String,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(generateText = openId)) val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
} }
} }
@@ -158,36 +119,5 @@ object MineAgentViewModel : ViewModel() {
} }
} }
// 添加新创建的智能体到列表顶部
fun addAgentToList(agent: AgentEntity) {
agentList = listOf(agent) + agentList
}
// 预加载图片,避免滑动时重复加载
fun preloadImages(context: android.content.Context) {
viewModelScope.launch {
agentList.forEach { agent ->
if (agent.id !in preloadedImageIds && agent.avatar.isNotEmpty()) {
try {
// 预加载头像图片到缓存
com.aiosman.ravenow.utils.Utils.getImageLoader(context).enqueue(
coil.request.ImageRequest.Builder(context)
.data(agent.avatar)
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
.build()
)
preloadedImageIds.add(agent.id)
} catch (e: Exception) {
// 忽略预加载错误
}
}
}
}
}
// 清除预加载记录(在刷新时调用)
fun clearPreloadedImages() {
preloadedImageIds.clear()
}
} }

View File

@@ -175,7 +175,7 @@ fun FriendChatItem(
CustomAsyncImage( CustomAsyncImage(
context = LocalContext.current, context = LocalContext.current,
imageUrl = conversation.avatar, imageUrl = conversation.avatar,
contentDescription = conversation.nickname, contentDescription = conversation.trtcUserId,
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.clip(RoundedCornerShape(48.dp)) .clip(RoundedCornerShape(48.dp))

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre package com.aiosman.ravenow.ui.index.tabs.moment.tabs.expolre
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -49,6 +50,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.ravenow.AppStore import com.aiosman.ravenow.AppStore
@@ -73,6 +75,8 @@ data class BannerItem(
val backgroundImageUrl: String, val backgroundImageUrl: String,
val userCount: Int, val userCount: Int,
val agentName: String, val agentName: String,
val trtcId: String,
val avatar: String
) { ) {
companion object { companion object {
fun fromRoom(room: Room): BannerItem { fun fromRoom(room: Room): BannerItem {
@@ -83,7 +87,18 @@ data class BannerItem(
imageUrl = "${ApiClient.RETROFIT_URL}${room.creator.profile.avatar}"+"?token="+"${AppStore.token}" ?: "", imageUrl = "${ApiClient.RETROFIT_URL}${room.creator.profile.avatar}"+"?token="+"${AppStore.token}" ?: "",
backgroundImageUrl = "${ApiClient.BASE_API_URL+"/outside"}${room.recommendBanner}"+"?token="+"${AppStore.token}" ?: "", backgroundImageUrl = "${ApiClient.BASE_API_URL+"/outside"}${room.recommendBanner}"+"?token="+"${AppStore.token}" ?: "",
userCount = room.userCount, userCount = room.userCount,
agentName = room.creator.profile.nickname agentName = room.creator.profile.nickname,
trtcId = room.trtcRoomId,
avatar = if (room.avatar.isNullOrEmpty()) {
// 将 groupId 转换为 Base64
val groupIdBase64 = android.util.Base64.encodeToString(
room.trtcType.toByteArray(),
android.util.Base64.NO_WRAP
)
"${ApiClient.RETROFIT_URL+"group/avatar?groupIdBase64="}${groupIdBase64}"+"&token="+"${AppStore.token}"
} else {
"${ApiClient.BASE_API_URL+"/outside/"}${room.avatar}"+"?token="+"${AppStore.token}"
}
) )
} }
} }
@@ -96,7 +111,7 @@ data class AgentItem(
val desc: String, val desc: String,
val avatar: String, val avatar: String,
val useCount: Int, val useCount: Int,
//val trtcId: String val openId: String
) { ) {
companion object { companion object {
fun fromAgent(agent: Agent): AgentItem { fun fromAgent(agent: Agent): AgentItem {
@@ -106,7 +121,7 @@ data class AgentItem(
desc = agent.desc, desc = agent.desc,
avatar = "${ApiClient.BASE_API_URL+"/outside"}${agent.avatar}"+"?token="+"${AppStore.token}", avatar = "${ApiClient.BASE_API_URL+"/outside"}${agent.avatar}"+"?token="+"${AppStore.token}",
useCount = agent.useCount, useCount = agent.useCount,
// trtcId = agent. openId = agent.openId,
) )
} }
} }
@@ -126,6 +141,9 @@ fun Explore() {
// 模拟刷新状态 // 模拟刷新状态
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
val enterSuccessText = stringResource(R.string.group_room_enter_success)
val enterFailText = stringResource(R.string.group_room_enter_fail) // 假设有这个资源
// 监听ViewModel的刷新状态 // 监听ViewModel的刷新状态
LaunchedEffect(viewModel.isRefreshing) { LaunchedEffect(viewModel.isRefreshing) {
isRefreshing = viewModel.isRefreshing isRefreshing = viewModel.isRefreshing
@@ -218,12 +236,13 @@ fun Explore() {
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
) )
.clickable { .clickable {
// 聊天逻辑 viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi(agentItem.openId, navController = navController)
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = "聊天", text = stringResource(R.string.chat),
fontSize = 12.sp, fontSize = 12.sp,
color = AppColors.text, color = AppColors.text,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500 fontWeight = androidx.compose.ui.text.font.FontWeight.W500
@@ -320,7 +339,7 @@ fun Explore() {
@Composable @Composable
fun HotChatRoomGridItem(roomItem: BannerItem) { fun HotChatRoomGridItem(roomItem: BannerItem) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current
Row( Row(
modifier = Modifier modifier = Modifier
.padding(horizontal = 2.dp, vertical = 4.dp) .padding(horizontal = 2.dp, vertical = 4.dp)
@@ -332,7 +351,22 @@ fun Explore() {
4 -> Color(0x28af52de) 4 -> Color(0x28af52de)
else -> Color(0x28ffcc00) else -> Color(0x28ffcc00)
}, },
shape = RoundedCornerShape(12.dp)), shape = RoundedCornerShape(12.dp))
.clickable {
// 调用加入房间接口
viewModel.joinRoom(
trtcId = roomItem.trtcId.toString(),
name = roomItem.title,
avatar = roomItem.avatar,
navController = navController,
onSuccess = {
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
},
onError = { errorMessage ->
Toast.makeText(context, enterFailText, Toast.LENGTH_SHORT).show()
}
)
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -427,9 +461,10 @@ fun Explore() {
@Composable @Composable
fun BannerCard(bannerItem: BannerItem, modifier: Modifier = Modifier) { fun BannerCard(bannerItem: BannerItem, viewModel: ExploreViewModel, modifier: Modifier = Modifier) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current
Card( Card(
modifier = modifier modifier = modifier
@@ -580,12 +615,24 @@ fun Explore() {
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
) )
.clickable { .clickable {
// 入房间逻辑 // 调用加入房间接口
viewModel.joinRoom(
trtcId = bannerItem.trtcId.toString(),
name = bannerItem.title,
avatar = bannerItem.avatar,
navController = navController,
onSuccess = {
Toast.makeText(context, enterSuccessText, Toast.LENGTH_SHORT).show()
},
onError = { errorMessage ->
Toast.makeText(context, enterFailText, Toast.LENGTH_SHORT).show()
}
)
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = "进入", text = stringResource(R.string.group_room_enter),
fontSize = 14.sp, fontSize = 14.sp,
color = Color.White, color = Color.White,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600 fontWeight = androidx.compose.ui.text.font.FontWeight.W600
@@ -806,6 +853,7 @@ fun Explore() {
BannerCard( BannerCard(
bannerItem = bannerItem, bannerItem = bannerItem,
viewModel = viewModel,
modifier = Modifier modifier = Modifier
.graphicsLayer { .graphicsLayer {
scaleX = scale scaleX = scale

View File

@@ -5,10 +5,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.aiosman.ravenow.data.Room import com.aiosman.ravenow.data.Room
import com.aiosman.ravenow.data.Agent import com.aiosman.ravenow.data.Agent
import com.aiosman.ravenow.data.api.ApiClient import com.aiosman.ravenow.data.api.ApiClient
import com.aiosman.ravenow.data.api.RaveNowAPI import com.aiosman.ravenow.data.api.RaveNowAPI
import com.aiosman.ravenow.data.api.SingleChatRequestBody
import com.aiosman.ravenow.data.api.JoinGroupChatRequestBody
import com.aiosman.ravenow.ui.index.tabs.ai.tabs.mine.MineAgentViewModel.createGroup2ChatAi
import com.aiosman.ravenow.ui.index.tabs.message.MessageListViewModel.userService
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel
import com.aiosman.ravenow.ui.index.tabs.message.tab.GroupChatListViewModel.createGroupChat
import com.aiosman.ravenow.ui.navigateToGroupChat
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ExploreViewModel : ViewModel() { class ExploreViewModel : ViewModel() {
@@ -122,5 +130,57 @@ class ExploreViewModel : ViewModel() {
} }
} }
} }
fun createSingleChat(
openId: String,
) {
viewModelScope.launch {
val response = ApiClient.api.createSingleChat(SingleChatRequestBody(agentOpenId = openId))
}
}
fun goToChatAi(
openId: String,
navController: NavHostController
) {
viewModelScope.launch {
val profile = userService.getUserProfileByOpenId(openId)
createGroup2ChatAi(profile.trtcUserId,"ai_group",navController,profile.id)
}
}
fun joinRoom(
trtcId: String,
name: String,
avatar: String,
navController: NavHostController,
onSuccess: () -> Unit,
onError: (String) -> Unit
) {
viewModelScope.launch {
try {
val response = apiClient.joinRoom(JoinGroupChatRequestBody(trtcId = trtcId))
if (response.isSuccessful) {
viewModelScope.launch {
try {
createGroupChat(trtcGroupId = trtcId)
// 群聊直接使用群ID进行导航
navController.navigateToGroupChat( id = trtcId,
name = name,
avatar = avatar)
} catch (e: Exception) {
onError("加入房间失败")
e.printStackTrace()
}
}
onSuccess()
} else {
onError("加入房间失败")
}
} catch (e: Exception) {
onError("网络请求失败:${e.message}")
}
}
}
} }

View File

@@ -58,7 +58,7 @@ fun HotMomentsList() {
val navigationBarPaddings = val navigationBarPaddings =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
HotMomentViewModel.refreshPager() model.refreshPager()
} }
var refreshing by remember { mutableStateOf(false) } var refreshing by remember { mutableStateOf(false) }
val state = rememberPullRefreshState(refreshing, onRefresh = { val state = rememberPullRefreshState(refreshing, onRefresh = {

View File

@@ -182,5 +182,8 @@
<string name="create_agent">创建智能体</string> <string name="create_agent">创建智能体</string>
<string name="publish_dynamic">发布动态</string> <string name="publish_dynamic">发布动态</string>
<string name="hot_agent">热门智能体</string> <string name="hot_agent">热门智能体</string>
<string name="group_room_enter">进入</string>
<string name="group_room_enter_success">成功加入房间</string>
<string name="group_room_enter_fail">加入房间失败</string>
</resources> </resources>

View File

@@ -177,5 +177,8 @@
<string name="create_agent">创建智能体</string> <string name="create_agent">创建智能体</string>
<string name="publish_dynamic">发布动态</string> <string name="publish_dynamic">发布动态</string>
<string name="hot_agent">热门智能体</string> <string name="hot_agent">热门智能体</string>
<string name="group_room_enter">进入</string>
<string name="group_room_enter_success">成功加入房间</string>
<string name="group_room_enter_fail">加入房间失败</string>
</resources> </resources>