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:
parent
39b5c3e40f
commit
0a0491ca1b
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@ -4,7 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<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">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=6fbe7ac" />
|
||||
|
||||
@ -44,6 +44,9 @@ fun App() {
|
||||
onImageClick = { postId, imageIndex ->
|
||||
navController.navigate(Screen.ImageDetail.createRoute(postId, imageIndex))
|
||||
},
|
||||
onAuthorClick = { authorId ->
|
||||
navController.navigate(Screen.UserDetail.createRoute(authorId))
|
||||
},
|
||||
sharedTransitionScope = this@SharedTransitionLayout,
|
||||
animatedContentScope = this@composable
|
||||
)
|
||||
@ -173,6 +176,7 @@ fun MainFlow(
|
||||
onLoginClick: () -> Unit,
|
||||
onPostClick: (Int) -> Unit,
|
||||
onImageClick: (Int, Int) -> Unit = { _, _ -> },
|
||||
onAuthorClick: (Int) -> Unit = {},
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedContentScope: AnimatedContentScope? = null
|
||||
) {
|
||||
@ -216,9 +220,7 @@ fun MainFlow(
|
||||
composable(Screen.Talk.route) {
|
||||
TalkScreen(
|
||||
onPostClick = onPostClick,
|
||||
onAuthorClick = { authorId ->
|
||||
// TODO: 导航到用户详情页
|
||||
},
|
||||
onAuthorClick = onAuthorClick,
|
||||
onImageClick = onImageClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope
|
||||
|
||||
@ -4,6 +4,8 @@ import com.qingshuige.tangyuan.api.ApiInterface
|
||||
import com.qingshuige.tangyuan.model.CreateUserDto
|
||||
import com.qingshuige.tangyuan.model.LoginDto
|
||||
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 retrofit2.awaitResponse
|
||||
@ -72,4 +74,24 @@ class UserRepository @Inject constructor(
|
||||
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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -36,12 +36,17 @@ import com.qingshuige.tangyuan.utils.withPanguSpacing
|
||||
/**
|
||||
* 评论项组件
|
||||
*/
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun CommentItem(
|
||||
comment: CommentCard,
|
||||
onReplyToComment: (CommentCard) -> Unit = {},
|
||||
onDeleteComment: (Int) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
onAuthorClick: (Int) -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedContentScope: AnimatedContentScope? = null,
|
||||
sharedElementPrefix: String = "comment"
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
@ -62,7 +67,11 @@ fun CommentItem(
|
||||
CommentMainContent(
|
||||
comment = comment,
|
||||
onReplyToComment = onReplyToComment,
|
||||
onDeleteComment = onDeleteComment
|
||||
onDeleteComment = onDeleteComment,
|
||||
onAuthorClick = onAuthorClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope,
|
||||
sharedElementPrefix = sharedElementPrefix
|
||||
)
|
||||
|
||||
// 回复列表
|
||||
@ -81,15 +90,26 @@ fun CommentItem(
|
||||
/**
|
||||
* 评论主体内容
|
||||
*/
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun CommentMainContent(
|
||||
comment: CommentCard,
|
||||
onReplyToComment: (CommentCard) -> Unit,
|
||||
onDeleteComment: (Int) -> Unit
|
||||
onDeleteComment: (Int) -> Unit,
|
||||
onAuthorClick: (Int) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedContentScope: AnimatedContentScope? = null,
|
||||
sharedElementPrefix: String = "comment"
|
||||
) {
|
||||
Column {
|
||||
// 评论头部 - 用户信息
|
||||
CommentHeader(comment = comment)
|
||||
CommentHeader(
|
||||
comment = comment,
|
||||
onAuthorClick = onAuthorClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope,
|
||||
sharedElementPrefix = sharedElementPrefix
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@ -123,10 +143,18 @@ private fun CommentMainContent(
|
||||
/**
|
||||
* 评论头部 - 用户信息
|
||||
*/
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun CommentHeader(comment: CommentCard) {
|
||||
private fun CommentHeader(
|
||||
comment: CommentCard,
|
||||
onAuthorClick: (Int) -> Unit = {},
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedContentScope: AnimatedContentScope? = null,
|
||||
sharedElementPrefix: String = "comment"
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable { onAuthorClick(comment.authorId) }
|
||||
) {
|
||||
// 用户头像
|
||||
AsyncImage(
|
||||
@ -137,6 +165,19 @@ private fun CommentHeader(comment: CommentCard) {
|
||||
contentDescription = "${comment.authorName}的头像",
|
||||
modifier = Modifier
|
||||
.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),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
@ -154,7 +195,18 @@ private fun CommentHeader(comment: CommentCard) {
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -136,7 +136,8 @@ fun PostCardItem(
|
||||
onImageClick: (Int, Int) -> Unit = { _, _ -> },
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedContentScope: AnimatedContentScope? = null
|
||||
animatedContentScope: AnimatedContentScope? = null,
|
||||
sharedElementPrefix: String = "postcard" // 添加前缀来区分不同位置的头像
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
@ -176,7 +177,8 @@ fun PostCardItem(
|
||||
onAuthorClick = onAuthorClick,
|
||||
onMoreClick = onMoreClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope
|
||||
animatedContentScope = animatedContentScope,
|
||||
sharedElementPrefix = sharedElementPrefix
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
@ -225,7 +227,8 @@ private fun PostCardHeader(
|
||||
onAuthorClick: (Int) -> Unit,
|
||||
onMoreClick: (Int) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedContentScope: AnimatedContentScope? = null
|
||||
animatedContentScope: AnimatedContentScope? = null,
|
||||
sharedElementPrefix: String = "postcard"
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -237,12 +240,11 @@ private fun PostCardHeader(
|
||||
contentDescription = "${postCard.authorName}的头像",
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.let { mod ->
|
||||
if (sharedTransitionScope != null && animatedContentScope != null) {
|
||||
with(sharedTransitionScope) {
|
||||
mod.sharedElement(
|
||||
rememberSharedContentState(key = "user_avatar_${postCard.authorId}"),
|
||||
rememberSharedContentState(key = "${sharedElementPrefix}_user_avatar_${postCard.authorId}"),
|
||||
animatedVisibilityScope = animatedContentScope,
|
||||
boundsTransform = { _, _ ->
|
||||
tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
||||
@ -250,7 +252,8 @@ private fun PostCardHeader(
|
||||
)
|
||||
}
|
||||
} else mod
|
||||
},
|
||||
}
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop,
|
||||
onClick = { onAuthorClick(postCard.authorId) }
|
||||
)
|
||||
@ -269,7 +272,18 @@ private fun PostCardHeader(
|
||||
fontFamily = TangyuanGeneralFontFamily,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
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(
|
||||
|
||||
@ -113,7 +113,9 @@ fun ImageDetailScreen(
|
||||
BottomContentOverlay(
|
||||
postCard = postCard,
|
||||
onAuthorClick = onAuthorClick,
|
||||
onSwitchToTextMode = onSwitchToTextMode
|
||||
onSwitchToTextMode = onSwitchToTextMode,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -313,11 +315,14 @@ private fun ZoomableImage(
|
||||
/**
|
||||
* 底部内容遮罩
|
||||
*/
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun BottomContentOverlay(
|
||||
postCard: PostCard,
|
||||
onAuthorClick: (Int) -> Unit,
|
||||
onSwitchToTextMode: () -> Unit
|
||||
onSwitchToTextMode: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope?,
|
||||
animatedContentScope: AnimatedContentScope?
|
||||
) {
|
||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||
val swipeThreshold = -100f // 上滑超过100px触发切换
|
||||
@ -356,7 +361,9 @@ private fun BottomContentOverlay(
|
||||
// 作者信息
|
||||
PostAuthorInfo(
|
||||
postCard = postCard,
|
||||
onAuthorClick = onAuthorClick
|
||||
onAuthorClick = onAuthorClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@ -424,10 +431,13 @@ private fun BottomContentOverlay(
|
||||
/**
|
||||
* 作者信息组件
|
||||
*/
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun PostAuthorInfo(
|
||||
postCard: PostCard,
|
||||
onAuthorClick: (Int) -> Unit
|
||||
onAuthorClick: (Int) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope?,
|
||||
animatedContentScope: AnimatedContentScope?
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -441,6 +451,19 @@ private fun PostAuthorInfo(
|
||||
contentDescription = "${postCard.authorName}的头像",
|
||||
modifier = Modifier
|
||||
.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),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
@ -453,7 +476,18 @@ private fun PostAuthorInfo(
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontFamily = TangyuanGeneralFontFamily,
|
||||
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()) {
|
||||
|
||||
@ -234,7 +234,11 @@ private fun PostDetailContent(
|
||||
CommentItem(
|
||||
comment = comment,
|
||||
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(
|
||||
postCard = postCard,
|
||||
onAuthorClick = onAuthorClick
|
||||
onAuthorClick = onAuthorClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@ -354,10 +360,13 @@ private fun PostDetailCard(
|
||||
/**
|
||||
* 帖子详情头部
|
||||
*/
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun PostDetailHeader(
|
||||
postCard: PostCard,
|
||||
onAuthorClick: (Int) -> Unit
|
||||
onAuthorClick: (Int) -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope?,
|
||||
animatedContentScope: AnimatedContentScope?
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -371,6 +380,19 @@ private fun PostDetailHeader(
|
||||
contentDescription = "${postCard.authorName}的头像",
|
||||
modifier = Modifier
|
||||
.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),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
@ -383,7 +405,18 @@ private fun PostDetailHeader(
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontFamily = TangyuanGeneralFontFamily,
|
||||
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()) {
|
||||
|
||||
@ -156,7 +156,8 @@ private fun PostList(
|
||||
onMoreClick = onMoreClick,
|
||||
onImageClick = onImageClick,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedContentScope = animatedContentScope
|
||||
animatedContentScope = animatedContentScope,
|
||||
sharedElementPrefix = "talk_post_${postCard.postId}" // 使用帖子ID作为唯一前缀
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,11 @@ package com.qingshuige.tangyuan.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.User
|
||||
import com.qingshuige.tangyuan.model.PostCard
|
||||
import com.qingshuige.tangyuan.repository.UserRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -11,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.async
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@ -22,9 +26,9 @@ class UserDetailViewModel @Inject constructor(
|
||||
private val _user = MutableStateFlow<User?>(null)
|
||||
val user: StateFlow<User?> = _user.asStateFlow()
|
||||
|
||||
// 用户帖子列表状态
|
||||
private val _userPosts = MutableStateFlow<List<PostMetadata>>(emptyList())
|
||||
val userPosts: StateFlow<List<PostMetadata>> = _userPosts.asStateFlow()
|
||||
// 用户帖子列表状态 - 改为PostCard列表
|
||||
private val _userPosts = MutableStateFlow<List<PostCard>>(emptyList())
|
||||
val userPosts: StateFlow<List<PostCard>> = _userPosts.asStateFlow()
|
||||
|
||||
// 加载状态
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
@ -55,15 +59,15 @@ class UserDetailViewModel @Inject constructor(
|
||||
_user.value = userInfo
|
||||
_isLoading.value = false
|
||||
// 获取用户信息成功后,加载用户的帖子
|
||||
loadUserPosts(userId)
|
||||
loadUserPosts(userId, userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户的帖子列表
|
||||
* 加载用户的帖子列表,包含完整的PostCard信息
|
||||
*/
|
||||
private fun loadUserPosts(userId: Int) {
|
||||
private fun loadUserPosts(userId: Int, user: User) {
|
||||
viewModelScope.launch {
|
||||
_isPostsLoading.value = true
|
||||
|
||||
@ -73,7 +77,20 @@ class UserDetailViewModel @Inject constructor(
|
||||
_isPostsLoading.value = false
|
||||
}
|
||||
.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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user