修复动态内容为空时的崩溃问题并优化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 momentTextContent: String,
val momentTextContent: String?,
// 动态图片
@DrawableRes val momentPicture: Int,
// 点赞数

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package com.aiosman.ravenow.ui.index.tabs.moment.tabs.recommend
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -12,6 +13,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.LocalNavController
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.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.navigateToChatAi
import kotlinx.coroutines.launch
/**
* Prompt推荐Item组件
@@ -43,11 +49,47 @@ fun PromptRecommendationItem(
) {
val AppColors = LocalAppTheme.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(
modifier = modifier
.fillMaxSize()
.background(Color.Black)
.clickable(
onClick = { navigateToProfile() }
)
) {
// 背景大图
CustomAsyncImage(
@@ -122,8 +164,8 @@ fun PromptRecommendationItem(
// Start chatting 按钮
Button(
onClick = {
// 导航到聊天页面
navController.navigate("${NavigationRoute.Chat.route}/$openId")
// 导航到AI聊天页面区分AI聊天和普通聊天
navigateToChatAi()
},
modifier = Modifier
.fillMaxWidth()

View File

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

View File

@@ -178,7 +178,7 @@ fun TimeGroup(time: String = "2024.06.08 12:23") {
@Composable
fun ProfileMomentCard(
content: String,
content: String?,
imageUrl: String,
like: String,
comment: String,
@@ -220,7 +220,7 @@ fun ProfileMomentCard(
columnHeight = coordinates.size.height
}
) {
if (content.isNotEmpty()) {
if (!content.isNullOrEmpty()) {
MomentCardTopContent(content)
}
MomentCardPicture(imageUrl, momentEntity = momentEntity)
@@ -237,7 +237,7 @@ fun ProfileMomentCard(
}
@Composable
fun MomentCardTopContent(content: String) {
fun MomentCardTopContent(content: String?) {
val AppColors = LocalAppTheme.current
Row(
modifier = Modifier
@@ -247,7 +247,7 @@ fun MomentCardTopContent(content: String) {
) {
Text(
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,
style = TextStyle(fontWeight = FontWeight.Bold)
)
if (moment.momentTextContent.isNotEmpty()) {
if (!moment.momentTextContent.isNullOrEmpty()) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
text = moment.momentTextContent,
text = moment.momentTextContent ?: "",
fontSize = 16.sp,
color = Color.White,
style = TextStyle(fontWeight = FontWeight.Bold),