修复动态内容为空时的崩溃问题并优化UI

- 将`Moment`实体中的`momentTextContent`字段类型从`String`修改为`String?`,以允许其为空,修复了多处因空内容引发的崩溃。
- 在多个UI组件中(如新闻、短视频、推荐等)添加了对`momentTextContent`的空值检查。
- 优化了“发现”页中智能体(Agent)卡片的UI样式,使用大图背景和渐变效果,并调整了按钮和文本布局。
- 为图片加载组件(`CustomAsyncImage`)增加了默认占位图,提升了加载过程中的用户体验。
- 在热门动态列表中,过滤掉没有图片的动态,确保UI显示正常。
- 修复了Prompt推荐页面的用户资料和AI聊天导航逻辑,并增加了防崩溃处理。
This commit is contained in:
2025-11-11 15:23:32 +08:00
parent 904cda3ae8
commit 791f5c4c96
12 changed files with 136 additions and 124 deletions

View File

@@ -327,7 +327,7 @@ data class MomentEntity(
// 是否关注 // 是否关注
val followStatus: Boolean, val followStatus: Boolean,
// 动态内容 // 动态内容
val momentTextContent: String, val momentTextContent: String?,
// 动态图片 // 动态图片
@DrawableRes val momentPicture: Int, @DrawableRes val momentPicture: Int,
// 点赞数 // 点赞数

View File

@@ -73,7 +73,8 @@ fun AgentCard(
agentEntity.avatar, agentEntity.avatar,
contentDescription = agentEntity.openId, contentDescription = agentEntity.openId,
modifier = Modifier.size(40.dp), modifier = Modifier.size(40.dp),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop,
defaultRes = com.aiosman.ravenow.R.mipmap.group_copy
) )
} }
Column( Column(

View File

@@ -379,9 +379,9 @@ fun MomentContentGroup(
) )
} }
} }
if (momentEntity.momentTextContent.isNotEmpty()) { if (!momentEntity.momentTextContent.isNullOrEmpty()) {
Text( Text(
text = momentEntity.momentTextContent, text = momentEntity.momentTextContent ?: "",
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 8.dp), .padding(start = 16.dp, end = 16.dp, top = 8.dp),

View File

@@ -467,8 +467,7 @@ fun AgentCardSquare(
navController: NavHostController navController: NavHostController
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val cardHeight = 180.dp val cardHeight = 210.dp
val avatarSize = cardHeight / 3 // 头像大小为方块高度的三分之一
// 防抖状态 // 防抖状态
var lastClickTime by remember { mutableStateOf(0L) } var lastClickTime by remember { mutableStateOf(0L) }
@@ -477,96 +476,76 @@ fun AgentCardSquare(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(cardHeight) .height(cardHeight)
.background(AppColors.secondaryBackground, RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable { .noRippleClickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
viewModel.goToProfile(agentItem.openId, navController) viewModel.goToProfile(agentItem.openId, navController)
}) { }) {
lastClickTime = System.currentTimeMillis() lastClickTime = System.currentTimeMillis()
} }
}, }
contentAlignment = Alignment.TopCenter
) { ) {
Box( // 背景大图
modifier = Modifier
.offset(y = 4.dp)
.size(avatarSize)
.background(AppColors.background, RoundedCornerShape(avatarSize / 2))
.clip(RoundedCornerShape(avatarSize / 2)),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.mipmap.group_copy),
contentDescription = "默认头像",
modifier = Modifier.size(avatarSize),
)
if (agentItem.avatar.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = agentItem.avatar, imageUrl = agentItem.avatar,
contentDescription = "Agent头像", contentDescription = agentItem.title,
modifier = Modifier modifier = Modifier.fillMaxSize(),
.size(avatarSize) contentScale = androidx.compose.ui.layout.ContentScale.Crop,
.clip(RoundedCornerShape(avatarSize / 2)), defaultRes = R.mipmap.rider_pro_agent
contentScale = androidx.compose.ui.layout.ContentScale.Crop
) )
}
}
// 内容区域(名称和描述) // 底部渐变与文字
Box(
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xB2000000)
)
)
.padding(12.dp)
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 4.dp + avatarSize + 8.dp, start = 8.dp, end = 8.dp, bottom = 48.dp), // 为底部聊天按钮留空间 .padding(bottom = 40.dp) // 为底部聊天按钮留空间
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = agentItem.title, text = agentItem.title,
color = Color.White,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600, fontWeight = androidx.compose.ui.text.font.FontWeight.W700,
color = AppColors.text,
maxLines = 1, maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(8.dp))
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = agentItem.desc, text = agentItem.desc,
fontSize = 12.sp, color = Color.White.copy(alpha = 0.92f),
color = AppColors.secondaryText, fontSize = 11.sp,
maxLines = 2, maxLines = 2,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
modifier = Modifier.weight(1f, fill = false)
) )
} }
}
// 聊天按钮 // 底部居中 Chat 按钮
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
.width(60.dp) .width(70.dp)
.height(32.dp) .height(32.dp)
.background( .background(AppColors.text, RoundedCornerShape(16.dp))
color = AppColors.text, .noRippleClickable {
shape = RoundedCornerShape(
topStart = 14.dp,
topEnd = 14.dp,
bottomStart = 0.dp,
bottomEnd = 14.dp
)
)
.clickable {
if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) { if (DebounceUtils.simpleDebounceClick(lastClickTime, 500L) {
// 检查游客模式,如果是游客则跳转登录
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.CHAT_WITH_AGENT)) {
navController.navigate(NavigationRoute.Login.route) navController.navigate(NavigationRoute.Login.route)
} else { } else {
viewModel.createSingleChat(agentItem.openId) viewModel.createSingleChat(agentItem.openId)
viewModel.goToChatAi( viewModel.goToChatAi(agentItem.openId, navController)
agentItem.openId,
navController = navController
)
} }
}) { }) {
lastClickTime = System.currentTimeMillis() lastClickTime = System.currentTimeMillis()
@@ -576,9 +555,9 @@ fun AgentCardSquare(
) { ) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = stringResource(R.string.chat), text = stringResource(R.string.chat),
fontSize = 15.sp,
color = AppColors.background, color = AppColors.background,
fontWeight = androidx.compose.ui.text.font.FontWeight.W500 fontSize = 13.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.W600
) )
} }
} }
@@ -658,7 +637,8 @@ fun AgentLargeCard(
imageUrl = agentItem.avatar, imageUrl = agentItem.avatar,
contentDescription = agentItem.title, contentDescription = agentItem.title,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = androidx.compose.ui.layout.ContentScale.Crop contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
) )
// 底部渐变与文字 // 底部渐变与文字
@@ -776,23 +756,16 @@ fun AgentCard2(viewModel: AgentViewModel,agentItem: AgentItem,navController: Nav
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Image(
painter = painterResource(R.mipmap.group_copy),
contentDescription = "默认头像",
modifier = Modifier.size(48.dp),
)
if (agentItem.avatar.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = agentItem.avatar, imageUrl = agentItem.avatar,
contentDescription = "Agent头像", contentDescription = "Agent头像",
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.clip(RoundedCornerShape(24.dp)), .clip(RoundedCornerShape(24.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.group_copy
) )
} }
}
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
@@ -998,7 +971,6 @@ fun ChatRoomCard(
// 优先显示banner如果没有banner则显示头像 // 优先显示banner如果没有banner则显示头像
val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar val imageUrl = if (chatRoom.banner.isNotEmpty()) chatRoom.banner else chatRoom.avatar
if (imageUrl.isNotEmpty()) {
CustomAsyncImage( CustomAsyncImage(
imageUrl = imageUrl, imageUrl = imageUrl,
contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像", contentDescription = if (chatRoom.banner.isNotEmpty()) "房间banner" else "房间头像",
@@ -1010,17 +982,9 @@ fun ChatRoomCard(
topEnd = 12.dp, topEnd = 12.dp,
bottomStart = 0.dp, bottomStart = 0.dp,
bottomEnd = 0.dp)), bottomEnd = 0.dp)),
contentScale = androidx.compose.ui.layout.ContentScale.Crop contentScale = androidx.compose.ui.layout.ContentScale.Crop,
defaultRes = R.mipmap.rider_pro_agent
) )
} else {
// 默认房间图标
Image(
painter = painterResource(R.mipmap.rider_pro_agent),
contentDescription = "默认房间图标",
modifier = Modifier.size(cardSize * 0.4f),
colorFilter = ColorFilter.tint(AppColors.secondaryText)
)
}
// 房间名称,重叠在底部 // 房间名称,重叠在底部
Box( Box(

View File

@@ -136,22 +136,27 @@ fun DiscoverView() {
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 4.dp) contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 4.dp)
) { ) {
items(moments) { momentItem -> items(moments) { momentItem ->
// 如果图片列表为空,跳过这个 item
if (momentItem.images.isEmpty()) {
return@items
}
val debouncer = rememberDebouncer() val debouncer = rememberDebouncer()
val textContent = momentItem.momentTextContent val textContent = momentItem.momentTextContent
// 对于英文和日文,每行字符数会更少,使用更保守的估算 // 对于英文和日文,每行字符数会更少,使用更保守的估算
val estimatedCharsPerLine = if (textContent.isNotEmpty()) { val estimatedCharsPerLine = if (!textContent.isNullOrEmpty()) {
// 检测是否包含非中文字符(英文、日文等) // 检测是否包含非中文字符(英文、日文等)
val hasNonChinese = textContent.any { val hasNonChinese = textContent?.any {
val code = it.code val code = it.code
!(code >= 0x4E00 && code <= 0x9FFF) // 不在中文字符范围内 !(code >= 0x4E00 && code <= 0x9FFF) // 不在中文字符范围内
} } ?: false
if (hasNonChinese) 15 else 20 // 英文/日文每行更少字符 if (hasNonChinese) 15 else 20 // 英文/日文每行更少字符
} else { } else {
20 20
} }
val textLines = if (textContent.isNotEmpty()) { val textLines = if (!textContent.isNullOrEmpty()) {
val estimatedLines = (textContent.length / estimatedCharsPerLine) + 1 val estimatedLines = ((textContent?.length ?: 0) / estimatedCharsPerLine) + 1
minOf(estimatedLines, 2) // 最多2行 minOf(estimatedLines, 2) // 最多2行
} else { } else {
0 0
@@ -209,9 +214,9 @@ fun DiscoverView() {
.padding(horizontal = 8.dp, vertical = 8.dp) .padding(horizontal = 8.dp, vertical = 8.dp)
) { ) {
// 文本内容区域,限制最大高度 // 文本内容区域,限制最大高度
if (momentItem.momentTextContent.isNotEmpty()) { if (!momentItem.momentTextContent.isNullOrEmpty()) {
androidx.compose.material3.Text( androidx.compose.material3.Text(
text = momentItem.momentTextContent, text = momentItem.momentTextContent ?: "",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontSize = 12.sp, fontSize = 12.sp,
color = AppColors.text, color = AppColors.text,

View File

@@ -150,7 +150,7 @@ fun FullArticleModal(
// 帖子内容 // 帖子内容
NewsContent( NewsContent(
content = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent, content = if (moment.newsContent.isNotEmpty()) moment.newsContent else (moment.momentTextContent ?: ""),
images = moment.images, images = moment.images,
context = context context = context
) )

View File

@@ -288,7 +288,7 @@ fun NewsItem(
// 新闻内容(超出使用省略号) // 新闻内容(超出使用省略号)
Text( Text(
text = if (moment.newsContent.isNotEmpty()) moment.newsContent else moment.momentTextContent, text = if (moment.newsContent.isNotEmpty()) moment.newsContent else (moment.momentTextContent ?: ""),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),

View File

@@ -132,12 +132,12 @@ fun PostRecommendationItem(
} }
// 文字内容 // 文字内容
if (moment.momentTextContent.isNotEmpty()) { if (!moment.momentTextContent.isNullOrEmpty()) {
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.8f) .fillMaxWidth(0.8f)
.padding(top = 4.dp), .padding(top = 4.dp),
text = moment.momentTextContent, text = moment.momentTextContent ?: "",
fontSize = 16.sp, fontSize = 16.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold), style = TextStyle(fontWeight = FontWeight.Bold),

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -12,6 +13,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
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
@@ -25,8 +27,12 @@ import androidx.compose.ui.unit.sp
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.UserService
import com.aiosman.ravenow.data.UserServiceImpl
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.navigateToChatAi
import kotlinx.coroutines.launch
/** /**
* Prompt推荐Item组件 * Prompt推荐Item组件
@@ -43,11 +49,47 @@ fun PromptRecommendationItem(
) { ) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
val navController = LocalNavController.current val navController = LocalNavController.current
val scope = rememberCoroutineScope()
val userService: UserService = UserServiceImpl()
// 导航到个人资料
fun navigateToProfile() {
scope.launch {
try {
val profile = userService.getUserProfileByOpenId(openId)
// Prompt推荐的一定是AI账号直接传递isAiAccount = true
navController.navigate(
NavigationRoute.AccountProfile.route
.replace("{id}", profile.id.toString())
.replace("{isAiAccount}", "true")
)
} catch (e: Exception) {
// 处理错误,避免崩溃
e.printStackTrace()
}
}
}
// 导航到AI聊天
fun navigateToChatAi() {
scope.launch {
try {
val profile = userService.getUserProfileByOpenId(openId)
navController.navigateToChatAi(profile.id.toString())
} catch (e: Exception) {
// 处理错误,避免崩溃
e.printStackTrace()
}
}
}
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(Color.Black) .background(Color.Black)
.clickable(
onClick = { navigateToProfile() }
)
) { ) {
// 背景大图 // 背景大图
CustomAsyncImage( CustomAsyncImage(
@@ -122,8 +164,8 @@ fun PromptRecommendationItem(
// Start chatting 按钮 // Start chatting 按钮
Button( Button(
onClick = { onClick = {
// 导航到聊天页面 // 导航到AI聊天页面区分AI聊天和普通聊天
navController.navigate("${NavigationRoute.Chat.route}/$openId") navigateToChatAi()
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@@ -278,12 +278,12 @@ fun VideoRecommendationItem(
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
) )
if (moment.momentTextContent.isNotEmpty()) { if (!moment.momentTextContent.isNullOrEmpty()) {
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.8f) .fillMaxWidth(0.8f)
.padding(top = 4.dp), .padding(top = 4.dp),
text = moment.momentTextContent, text = moment.momentTextContent ?: "",
fontSize = 16.sp, fontSize = 16.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold), style = TextStyle(fontWeight = FontWeight.Bold),

View File

@@ -178,7 +178,7 @@ fun TimeGroup(time: String = "2024.06.08 12:23") {
@Composable @Composable
fun ProfileMomentCard( fun ProfileMomentCard(
content: String, content: String?,
imageUrl: String, imageUrl: String,
like: String, like: String,
comment: String, comment: String,
@@ -220,7 +220,7 @@ fun ProfileMomentCard(
columnHeight = coordinates.size.height columnHeight = coordinates.size.height
} }
) { ) {
if (content.isNotEmpty()) { if (!content.isNullOrEmpty()) {
MomentCardTopContent(content) MomentCardTopContent(content)
} }
MomentCardPicture(imageUrl, momentEntity = momentEntity) MomentCardPicture(imageUrl, momentEntity = momentEntity)
@@ -237,7 +237,7 @@ fun ProfileMomentCard(
} }
@Composable @Composable
fun MomentCardTopContent(content: String) { fun MomentCardTopContent(content: String?) {
val AppColors = LocalAppTheme.current val AppColors = LocalAppTheme.current
Row( Row(
modifier = Modifier modifier = Modifier
@@ -247,7 +247,7 @@ fun MomentCardTopContent(content: String) {
) { ) {
Text( Text(
modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp), modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp),
text = content, fontSize = 16.sp, color = AppColors.text text = content ?: "", fontSize = 16.sp, color = AppColors.text
) )
} }
} }

View File

@@ -447,12 +447,12 @@ fun VideoPlayer(
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold) style = TextStyle(fontWeight = FontWeight.Bold)
) )
if (moment.momentTextContent.isNotEmpty()) { if (!moment.momentTextContent.isNullOrEmpty()) {
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 4.dp), .padding(top = 4.dp),
text = moment.momentTextContent, text = moment.momentTextContent ?: "",
fontSize = 16.sp, fontSize = 16.sp,
color = Color.White, color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold), style = TextStyle(fontWeight = FontWeight.Bold),