refactor: Overhaul UserDetailScreen and enhance user post feed

This commit introduces a significant redesign of the `UserDetailScreen`, transforming it from a simple card-based layout to a more modern and refined profile view. It also replaces the basic list of user posts with a full-featured `PostCardItem` feed.

**Key Changes:**

*   **feat(UserDetailScreen):**
    *   Redesigned the user profile section with a cleaner, borderless layout, moving from a `Card` to a `Column`-based design.
    *   Enhanced user info display to include email and location as decorative "pills."
    *   Replaced the previous list of post metadata with a full `PostCardItem`-based feed, showing complete post content directly on the user's profile.
    *   Added loading skeletons for the profile and post list, as well as an improved empty state for users with no posts.
    *   Added a "bottom indicator" to signify the end of the post list.

*   **refactor(ViewModel):**
    *   Modified `UserDetailViewModel` to fetch and construct full `PostCard` objects for the user's posts, instead of just `PostMetadata`.
    *   This involves fetching `PostBody` and `Category` details for each post to provide a rich feed experience.

*   **refactor(Shared Element Transition):**
    *   Introduced `sharedElementPrefix` to `PostCardItem` and `UserDetailScreen` to create unique transition keys for elements (like avatars and names) that appear in multiple screens (e.g., `talk_post_...`, `userdetail_post_...`).
    *   This ensures that shared element animations are correctly scoped and avoids conflicts when navigating between different feeds and detail screens.
    *   The shared element transition for user avatar and name now works correctly from any `PostCard` to the `UserDetailScreen`.

*   **feat(Repository):**
    *   Added `getPostBody(postId)` and `getCategory(categoryId)` methods to `UserRepository` to support fetching the detailed data required for constructing `PostCard` objects.
This commit is contained in:
grtsinry43 2025-10-06 13:13:47 +08:00
parent 39b5c3e40f
commit 0a0491ca1b
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
10 changed files with 763 additions and 440 deletions

View File

@ -4,7 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-10-05T14:23:49.203872Z"> <DropdownSelection timestamp="2025-10-06T04:37:55.083829Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=6fbe7ac" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=6fbe7ac" />

View File

@ -44,6 +44,9 @@ fun App() {
onImageClick = { postId, imageIndex -> onImageClick = { postId, imageIndex ->
navController.navigate(Screen.ImageDetail.createRoute(postId, imageIndex)) navController.navigate(Screen.ImageDetail.createRoute(postId, imageIndex))
}, },
onAuthorClick = { authorId ->
navController.navigate(Screen.UserDetail.createRoute(authorId))
},
sharedTransitionScope = this@SharedTransitionLayout, sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@composable animatedContentScope = this@composable
) )
@ -173,6 +176,7 @@ fun MainFlow(
onLoginClick: () -> Unit, onLoginClick: () -> Unit,
onPostClick: (Int) -> Unit, onPostClick: (Int) -> Unit,
onImageClick: (Int, Int) -> Unit = { _, _ -> }, onImageClick: (Int, Int) -> Unit = { _, _ -> },
onAuthorClick: (Int) -> Unit = {},
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null animatedContentScope: AnimatedContentScope? = null
) { ) {
@ -216,9 +220,7 @@ fun MainFlow(
composable(Screen.Talk.route) { composable(Screen.Talk.route) {
TalkScreen( TalkScreen(
onPostClick = onPostClick, onPostClick = onPostClick,
onAuthorClick = { authorId -> onAuthorClick = onAuthorClick,
// TODO: 导航到用户详情页
},
onImageClick = onImageClick, onImageClick = onImageClick,
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope animatedContentScope = animatedContentScope

View File

@ -4,6 +4,8 @@ import com.qingshuige.tangyuan.api.ApiInterface
import com.qingshuige.tangyuan.model.CreateUserDto import com.qingshuige.tangyuan.model.CreateUserDto
import com.qingshuige.tangyuan.model.LoginDto import com.qingshuige.tangyuan.model.LoginDto
import com.qingshuige.tangyuan.model.User import com.qingshuige.tangyuan.model.User
import com.qingshuige.tangyuan.model.PostBody
import com.qingshuige.tangyuan.model.Category
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse import retrofit2.awaitResponse
@ -72,4 +74,24 @@ class UserRepository @Inject constructor(
throw Exception("Failed to get user posts: ${response.message()}") throw Exception("Failed to get user posts: ${response.message()}")
} }
} }
fun getPostBody(postId: Int): Flow<PostBody> = flow {
val response = apiInterface.getPostBody(postId).awaitResponse()
if (response.isSuccessful) {
response.body()?.let { emit(it) }
?: throw Exception("Post body not found")
} else {
throw Exception("Failed to get post body: ${response.message()}")
}
}
fun getCategory(categoryId: Int): Flow<Category> = flow {
val response = apiInterface.getCategory(categoryId).awaitResponse()
if (response.isSuccessful) {
response.body()?.let { emit(it) }
?: throw Exception("Category not found")
} else {
throw Exception("Failed to get category: ${response.message()}")
}
}
} }

View File

@ -36,12 +36,17 @@ import com.qingshuige.tangyuan.utils.withPanguSpacing
/** /**
* 评论项组件 * 评论项组件
*/ */
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
fun CommentItem( fun CommentItem(
comment: CommentCard, comment: CommentCard,
onReplyToComment: (CommentCard) -> Unit = {}, onReplyToComment: (CommentCard) -> Unit = {},
onDeleteComment: (Int) -> Unit = {}, onDeleteComment: (Int) -> Unit = {},
modifier: Modifier = Modifier onAuthorClick: (Int) -> Unit = {},
modifier: Modifier = Modifier,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null,
sharedElementPrefix: String = "comment"
) { ) {
Card( Card(
modifier = modifier modifier = modifier
@ -62,7 +67,11 @@ fun CommentItem(
CommentMainContent( CommentMainContent(
comment = comment, comment = comment,
onReplyToComment = onReplyToComment, onReplyToComment = onReplyToComment,
onDeleteComment = onDeleteComment onDeleteComment = onDeleteComment,
onAuthorClick = onAuthorClick,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope,
sharedElementPrefix = sharedElementPrefix
) )
// 回复列表 // 回复列表
@ -81,15 +90,26 @@ fun CommentItem(
/** /**
* 评论主体内容 * 评论主体内容
*/ */
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
private fun CommentMainContent( private fun CommentMainContent(
comment: CommentCard, comment: CommentCard,
onReplyToComment: (CommentCard) -> Unit, onReplyToComment: (CommentCard) -> Unit,
onDeleteComment: (Int) -> Unit onDeleteComment: (Int) -> Unit,
onAuthorClick: (Int) -> Unit,
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null,
sharedElementPrefix: String = "comment"
) { ) {
Column { Column {
// 评论头部 - 用户信息 // 评论头部 - 用户信息
CommentHeader(comment = comment) CommentHeader(
comment = comment,
onAuthorClick = onAuthorClick,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope,
sharedElementPrefix = sharedElementPrefix
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -123,10 +143,18 @@ private fun CommentMainContent(
/** /**
* 评论头部 - 用户信息 * 评论头部 - 用户信息
*/ */
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
private fun CommentHeader(comment: CommentCard) { private fun CommentHeader(
comment: CommentCard,
onAuthorClick: (Int) -> Unit = {},
sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null,
sharedElementPrefix: String = "comment"
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { onAuthorClick(comment.authorId) }
) { ) {
// 用户头像 // 用户头像
AsyncImage( AsyncImage(
@ -137,6 +165,19 @@ private fun CommentHeader(comment: CommentCard) {
contentDescription = "${comment.authorName}的头像", contentDescription = "${comment.authorName}的头像",
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
.let { mod ->
if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
mod.sharedElement(
rememberSharedContentState(key = "${sharedElementPrefix}_user_avatar_${comment.authorId}"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing)
}
)
}
} else mod
}
.clip(CircleShape), .clip(CircleShape),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
@ -154,7 +195,18 @@ private fun CommentHeader(comment: CommentCard) {
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis,
modifier = if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
Modifier.sharedElement(
rememberSharedContentState(key = "${sharedElementPrefix}_user_name_${comment.authorId}"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing)
}
)
}
} else Modifier
) )
} }

View File

@ -136,7 +136,8 @@ fun PostCardItem(
onImageClick: (Int, Int) -> Unit = { _, _ -> }, onImageClick: (Int, Int) -> Unit = { _, _ -> },
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null animatedContentScope: AnimatedContentScope? = null,
sharedElementPrefix: String = "postcard" // 添加前缀来区分不同位置的头像
) { ) {
Card( Card(
modifier = modifier modifier = modifier
@ -176,7 +177,8 @@ fun PostCardItem(
onAuthorClick = onAuthorClick, onAuthorClick = onAuthorClick,
onMoreClick = onMoreClick, onMoreClick = onMoreClick,
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope animatedContentScope = animatedContentScope,
sharedElementPrefix = sharedElementPrefix
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@ -225,7 +227,8 @@ private fun PostCardHeader(
onAuthorClick: (Int) -> Unit, onAuthorClick: (Int) -> Unit,
onMoreClick: (Int) -> Unit, onMoreClick: (Int) -> Unit,
sharedTransitionScope: SharedTransitionScope? = null, sharedTransitionScope: SharedTransitionScope? = null,
animatedContentScope: AnimatedContentScope? = null animatedContentScope: AnimatedContentScope? = null,
sharedElementPrefix: String = "postcard"
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -237,12 +240,11 @@ private fun PostCardHeader(
contentDescription = "${postCard.authorName}的头像", contentDescription = "${postCard.authorName}的头像",
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.clip(CircleShape)
.let { mod -> .let { mod ->
if (sharedTransitionScope != null && animatedContentScope != null) { if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) { with(sharedTransitionScope) {
mod.sharedElement( mod.sharedElement(
rememberSharedContentState(key = "user_avatar_${postCard.authorId}"), rememberSharedContentState(key = "${sharedElementPrefix}_user_avatar_${postCard.authorId}"),
animatedVisibilityScope = animatedContentScope, animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ -> boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing) tween(durationMillis = 400, easing = FastOutSlowInEasing)
@ -250,7 +252,8 @@ private fun PostCardHeader(
) )
} }
} else mod } else mod
}, }
.clip(CircleShape),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
onClick = { onAuthorClick(postCard.authorId) } onClick = { onAuthorClick(postCard.authorId) }
) )
@ -269,7 +272,18 @@ private fun PostCardHeader(
fontFamily = TangyuanGeneralFontFamily, fontFamily = TangyuanGeneralFontFamily,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis,
modifier = if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
Modifier.sharedElement(
rememberSharedContentState(key = "${sharedElementPrefix}_user_name_${postCard.authorId}"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing)
}
)
}
} else Modifier
) )
Text( Text(

View File

@ -113,7 +113,9 @@ fun ImageDetailScreen(
BottomContentOverlay( BottomContentOverlay(
postCard = postCard, postCard = postCard,
onAuthorClick = onAuthorClick, onAuthorClick = onAuthorClick,
onSwitchToTextMode = onSwitchToTextMode onSwitchToTextMode = onSwitchToTextMode,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
) )
} }
} }
@ -313,11 +315,14 @@ private fun ZoomableImage(
/** /**
* 底部内容遮罩 * 底部内容遮罩
*/ */
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
private fun BottomContentOverlay( private fun BottomContentOverlay(
postCard: PostCard, postCard: PostCard,
onAuthorClick: (Int) -> Unit, onAuthorClick: (Int) -> Unit,
onSwitchToTextMode: () -> Unit onSwitchToTextMode: () -> Unit,
sharedTransitionScope: SharedTransitionScope?,
animatedContentScope: AnimatedContentScope?
) { ) {
var offsetY by remember { mutableFloatStateOf(0f) } var offsetY by remember { mutableFloatStateOf(0f) }
val swipeThreshold = -100f // 上滑超过100px触发切换 val swipeThreshold = -100f // 上滑超过100px触发切换
@ -356,7 +361,9 @@ private fun BottomContentOverlay(
// 作者信息 // 作者信息
PostAuthorInfo( PostAuthorInfo(
postCard = postCard, postCard = postCard,
onAuthorClick = onAuthorClick onAuthorClick = onAuthorClick,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -424,10 +431,13 @@ private fun BottomContentOverlay(
/** /**
* 作者信息组件 * 作者信息组件
*/ */
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
private fun PostAuthorInfo( private fun PostAuthorInfo(
postCard: PostCard, postCard: PostCard,
onAuthorClick: (Int) -> Unit onAuthorClick: (Int) -> Unit,
sharedTransitionScope: SharedTransitionScope?,
animatedContentScope: AnimatedContentScope?
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -441,6 +451,19 @@ private fun PostAuthorInfo(
contentDescription = "${postCard.authorName}的头像", contentDescription = "${postCard.authorName}的头像",
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.let { mod ->
if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
mod.sharedElement(
rememberSharedContentState(key = "user_avatar_${postCard.authorId}"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing)
}
)
}
} else mod
}
.clip(CircleShape), .clip(CircleShape),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
@ -453,7 +476,18 @@ private fun PostAuthorInfo(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontFamily = TangyuanGeneralFontFamily, fontFamily = TangyuanGeneralFontFamily,
color = Color.White, color = Color.White,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
modifier = if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
Modifier.sharedElement(
rememberSharedContentState(key = "user_name_${postCard.authorId}"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing)
}
)
}
} else Modifier
) )
if (postCard.authorBio.isNotBlank()) { if (postCard.authorBio.isNotBlank()) {

View File

@ -234,7 +234,11 @@ private fun PostDetailContent(
CommentItem( CommentItem(
comment = comment, comment = comment,
onReplyToComment = onReplyToComment, onReplyToComment = onReplyToComment,
onDeleteComment = onDeleteComment onDeleteComment = onDeleteComment,
onAuthorClick = onAuthorClick,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope,
sharedElementPrefix = "postdetail_comment_${comment.commentId}"
) )
} }
@ -291,7 +295,9 @@ private fun PostDetailCard(
// 作者信息 // 作者信息
PostDetailHeader( PostDetailHeader(
postCard = postCard, postCard = postCard,
onAuthorClick = onAuthorClick onAuthorClick = onAuthorClick,
sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -354,10 +360,13 @@ private fun PostDetailCard(
/** /**
* 帖子详情头部 * 帖子详情头部
*/ */
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable @Composable
private fun PostDetailHeader( private fun PostDetailHeader(
postCard: PostCard, postCard: PostCard,
onAuthorClick: (Int) -> Unit onAuthorClick: (Int) -> Unit,
sharedTransitionScope: SharedTransitionScope?,
animatedContentScope: AnimatedContentScope?
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -371,6 +380,19 @@ private fun PostDetailHeader(
contentDescription = "${postCard.authorName}的头像", contentDescription = "${postCard.authorName}的头像",
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.let { mod ->
if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
mod.sharedElement(
rememberSharedContentState(key = "user_avatar_${postCard.authorId}"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing)
}
)
}
} else mod
}
.clip(CircleShape), .clip(CircleShape),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
@ -383,7 +405,18 @@ private fun PostDetailHeader(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontFamily = TangyuanGeneralFontFamily, fontFamily = TangyuanGeneralFontFamily,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
modifier = if (sharedTransitionScope != null && animatedContentScope != null) {
with(sharedTransitionScope) {
Modifier.sharedElement(
rememberSharedContentState(key = "user_name_${postCard.authorId}"),
animatedVisibilityScope = animatedContentScope,
boundsTransform = { _, _ ->
tween(durationMillis = 400, easing = FastOutSlowInEasing)
}
)
}
} else Modifier
) )
if (postCard.authorBio.isNotBlank()) { if (postCard.authorBio.isNotBlank()) {

View File

@ -156,7 +156,8 @@ private fun PostList(
onMoreClick = onMoreClick, onMoreClick = onMoreClick,
onImageClick = onImageClick, onImageClick = onImageClick,
sharedTransitionScope = sharedTransitionScope, sharedTransitionScope = sharedTransitionScope,
animatedContentScope = animatedContentScope animatedContentScope = animatedContentScope,
sharedElementPrefix = "talk_post_${postCard.postId}" // 使用帖子ID作为唯一前缀
) )
} }

View File

@ -2,8 +2,11 @@ package com.qingshuige.tangyuan.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.qingshuige.tangyuan.model.Category
import com.qingshuige.tangyuan.model.PostBody
import com.qingshuige.tangyuan.model.PostMetadata import com.qingshuige.tangyuan.model.PostMetadata
import com.qingshuige.tangyuan.model.User import com.qingshuige.tangyuan.model.User
import com.qingshuige.tangyuan.model.PostCard
import com.qingshuige.tangyuan.repository.UserRepository import com.qingshuige.tangyuan.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -11,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.async
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -22,9 +26,9 @@ class UserDetailViewModel @Inject constructor(
private val _user = MutableStateFlow<User?>(null) private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow() val user: StateFlow<User?> = _user.asStateFlow()
// 用户帖子列表状态 // 用户帖子列表状态 - 改为PostCard列表
private val _userPosts = MutableStateFlow<List<PostMetadata>>(emptyList()) private val _userPosts = MutableStateFlow<List<PostCard>>(emptyList())
val userPosts: StateFlow<List<PostMetadata>> = _userPosts.asStateFlow() val userPosts: StateFlow<List<PostCard>> = _userPosts.asStateFlow()
// 加载状态 // 加载状态
private val _isLoading = MutableStateFlow(false) private val _isLoading = MutableStateFlow(false)
@ -55,15 +59,15 @@ class UserDetailViewModel @Inject constructor(
_user.value = userInfo _user.value = userInfo
_isLoading.value = false _isLoading.value = false
// 获取用户信息成功后,加载用户的帖子 // 获取用户信息成功后,加载用户的帖子
loadUserPosts(userId) loadUserPosts(userId, userInfo)
} }
} }
} }
/** /**
* 加载用户的帖子列表 * 加载用户的帖子列表包含完整的PostCard信息
*/ */
private fun loadUserPosts(userId: Int) { private fun loadUserPosts(userId: Int, user: User) {
viewModelScope.launch { viewModelScope.launch {
_isPostsLoading.value = true _isPostsLoading.value = true
@ -73,7 +77,20 @@ class UserDetailViewModel @Inject constructor(
_isPostsLoading.value = false _isPostsLoading.value = false
} }
.collect { posts -> .collect { posts ->
_userPosts.value = posts // 并行获取每个帖子的完整信息
val postCards = posts.map { postMetadata ->
async {
try {
postMetadata.toPostCard(user, userRepository)
} catch (e: Exception) {
// 如果获取详细信息失败,返回简化版本
postMetadata.toSimplePostCard(user)
}
}
}.map { it.await() }
// 按时间倒序排序,新的在前面
_userPosts.value = postCards.sortedByDescending { it.postDateTime }
_isPostsLoading.value = false _isPostsLoading.value = false
} }
} }
@ -114,4 +131,100 @@ class UserDetailViewModel @Inject constructor(
} }
} }
} }
}
/**
* PostMetadata扩展函数转换为完整的PostCard
*/
private suspend fun PostMetadata.toPostCard(
author: User,
userRepository: UserRepository
): PostCard = kotlinx.coroutines.coroutineScope {
return@coroutineScope try {
// 并行获取PostBody和Category
val postBodyDeferred = async {
var result: PostBody? = null
userRepository.getPostBody(this@toPostCard.postId)
.catch { /* 忽略错误使用默认null值 */ }
.collect { result = it }
result
}
val categoryDeferred = async {
var result: Category? = null
userRepository.getCategory(this@toPostCard.categoryId)
.catch { /* 忽略错误使用默认null值 */ }
.collect { result = it }
result
}
val postBody = postBodyDeferred.await()
val category = categoryDeferred.await()
// 提取图片UUID列表
val imageUUIDs = listOfNotNull(
postBody?.image1UUID,
postBody?.image2UUID,
postBody?.image3UUID
).filter { it.isNotBlank() }
PostCard(
postId = this@toPostCard.postId,
postDateTime = this@toPostCard.postDateTime,
isVisible = this@toPostCard.isVisible,
authorId = author.userId,
authorName = author.nickName.ifBlank { "匿名用户" },
authorAvatar = author.avatarGuid,
authorBio = author.bio,
categoryId = this@toPostCard.categoryId,
categoryName = category?.baseName ?: "未分类",
categoryDescription = category?.baseDescription ?: "",
textContent = postBody?.textContent ?: "内容获取失败",
imageUUIDs = imageUUIDs,
hasImages = imageUUIDs.isNotEmpty(),
// 默认互动数据
likeCount = 0,
commentCount = 0,
shareCount = 0,
isLiked = false,
isBookmarked = false
)
} catch (e: Exception) {
// 如果获取详细信息失败,返回简化版本
this@toPostCard.toSimplePostCard(author)
}
}
/**
* PostMetadata扩展函数转换为简化的PostCard作为备用
*/
private fun PostMetadata.toSimplePostCard(author: User): PostCard {
return PostCard(
postId = this.postId,
postDateTime = this.postDateTime,
isVisible = this.isVisible,
authorId = author.userId,
authorName = author.nickName.ifBlank { "匿名用户" },
authorAvatar = author.avatarGuid,
authorBio = author.bio,
categoryId = this.categoryId,
categoryName = "分类 ${this.categoryId}", // 简化显示
categoryDescription = "",
textContent = "点击查看完整内容...",
imageUUIDs = emptyList(),
hasImages = false,
// 默认互动数据
likeCount = 0,
commentCount = 0,
shareCount = 0,
isLiked = false,
isBookmarked = false
)
} }