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>
|
<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" />
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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()) {
|
||||||
|
|||||||
@ -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()) {
|
||||||
|
|||||||
@ -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作为唯一前缀
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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.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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user