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作为唯一前缀
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,9 @@ package com.qingshuige.tangyuan.ui.screens
|
|||||||
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -19,29 +16,24 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.qingshuige.tangyuan.TangyuanApplication
|
import com.qingshuige.tangyuan.TangyuanApplication
|
||||||
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.ui.components.PostCardItem
|
||||||
import com.qingshuige.tangyuan.ui.components.ShimmerAsyncImage
|
import com.qingshuige.tangyuan.ui.components.ShimmerAsyncImage
|
||||||
import com.qingshuige.tangyuan.ui.theme.LiteraryFontFamily
|
import com.qingshuige.tangyuan.ui.theme.LiteraryFontFamily
|
||||||
import com.qingshuige.tangyuan.ui.theme.TangyuanGeneralFontFamily
|
import com.qingshuige.tangyuan.ui.theme.TangyuanGeneralFontFamily
|
||||||
import com.qingshuige.tangyuan.ui.theme.TangyuanShapes
|
|
||||||
import com.qingshuige.tangyuan.ui.theme.TangyuanTypography
|
import com.qingshuige.tangyuan.ui.theme.TangyuanTypography
|
||||||
import com.qingshuige.tangyuan.utils.withPanguSpacing
|
import com.qingshuige.tangyuan.utils.withPanguSpacing
|
||||||
import com.qingshuige.tangyuan.viewmodel.UserDetailViewModel
|
import com.qingshuige.tangyuan.viewmodel.UserDetailViewModel
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户详情页
|
* 用户详情页
|
||||||
@ -55,6 +47,7 @@ fun UserDetailScreen(
|
|||||||
onFollowClick: () -> Unit = {},
|
onFollowClick: () -> Unit = {},
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedContentScope: AnimatedContentScope? = null,
|
animatedContentScope: AnimatedContentScope? = null,
|
||||||
|
sharedElementPrefix: String? = null, // 从导航传递的前缀
|
||||||
viewModel: UserDetailViewModel = hiltViewModel()
|
viewModel: UserDetailViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val user by viewModel.user.collectAsState()
|
val user by viewModel.user.collectAsState()
|
||||||
@ -65,8 +58,9 @@ fun UserDetailScreen(
|
|||||||
|
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 初始加载
|
// 延迟初始加载以避免阻塞共享元素动画
|
||||||
LaunchedEffect(userId) {
|
LaunchedEffect(userId) {
|
||||||
|
kotlinx.coroutines.delay(200) // 等待共享元素动画完成
|
||||||
viewModel.loadUserDetails(userId)
|
viewModel.loadUserDetails(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +72,16 @@ fun UserDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
UserDetailTopBar(
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
userName = user?.nickName ?: "",
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { paddingValues ->
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
@ -85,54 +89,87 @@ fun UserDetailScreen(
|
|||||||
viewModel.refreshUserData(userId)
|
viewModel.refreshUserData(userId)
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(bottom = 100.dp)
|
contentPadding = PaddingValues(bottom = 100.dp)
|
||||||
) {
|
) {
|
||||||
// 顶部导航栏
|
// 用户信息区域
|
||||||
item {
|
|
||||||
UserDetailTopBar(
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
userName = user?.nickName ?: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户信息卡片
|
|
||||||
item {
|
item {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
UserDetailLoadingCard()
|
UserProfileLoadingState()
|
||||||
} else {
|
} else {
|
||||||
user?.let { userInfo ->
|
user?.let { userInfo ->
|
||||||
UserDetailCard(
|
UserProfileSection(
|
||||||
user = userInfo,
|
user = userInfo,
|
||||||
onFollowClick = onFollowClick,
|
onFollowClick = onFollowClick,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedContentScope = animatedContentScope
|
animatedContentScope = animatedContentScope,
|
||||||
|
sharedElementPrefix = sharedElementPrefix
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计信息
|
// 统计信息
|
||||||
item {
|
// item {
|
||||||
|
// user?.let { userInfo ->
|
||||||
|
// UserStatsSection(
|
||||||
|
// postsCount = userPosts.size
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 用户帖子信息流
|
||||||
user?.let { userInfo ->
|
user?.let { userInfo ->
|
||||||
UserStatsSection(
|
item {
|
||||||
postsCount = userPosts.size,
|
PostsSectionHeader(postsCount = userPosts.size)
|
||||||
user = userInfo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户帖子列表
|
if (isPostsLoading && userPosts.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
PostsSection(
|
PostsLoadingState()
|
||||||
posts = userPosts,
|
}
|
||||||
isLoading = isPostsLoading,
|
} else if (userPosts.isEmpty() && !isPostsLoading) {
|
||||||
onPostClick = onPostClick
|
item {
|
||||||
|
EmptyPostsState()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用PostCardItem展示完整帖子信息流
|
||||||
|
items(
|
||||||
|
items = userPosts,
|
||||||
|
key = { it.postId }
|
||||||
|
) { postCard ->
|
||||||
|
PostCardItem(
|
||||||
|
postCard = postCard,
|
||||||
|
onPostClick = onPostClick,
|
||||||
|
onAuthorClick = { /* 已经在用户详情页,不需要跳转 */ },
|
||||||
|
onLikeClick = { /* TODO: 实现点赞 */ },
|
||||||
|
onCommentClick = { /* TODO: 实现评论 */ },
|
||||||
|
onShareClick = { /* TODO: 实现分享 */ },
|
||||||
|
onBookmarkClick = { /* TODO: 实现收藏 */ },
|
||||||
|
onMoreClick = { /* TODO: 实现更多操作 */ },
|
||||||
|
onImageClick = { postId, imageIndex ->
|
||||||
|
// TODO: 实现图片点击
|
||||||
|
},
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedContentScope = animatedContentScope,
|
||||||
|
sharedElementPrefix = "userdetail_post_${postCard.postId}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 底部提示文字
|
||||||
|
if (userPosts.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
BottomIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,23 +181,33 @@ fun UserDetailScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun UserDetailTopBar(
|
private fun UserDetailTopBar(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
userName: String
|
userName: String,
|
||||||
|
isLoading: Boolean = false
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
|
if (isLoading) {
|
||||||
Text(
|
Text(
|
||||||
text = userName.withPanguSpacing(),
|
text = "用户详情",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = if (userName.isNotBlank()) "用户详情 · $userName" else "用户详情",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBackClick) {
|
IconButton(onClick = onBackClick) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
imageVector = Icons.Default.ArrowBack,
|
||||||
contentDescription = "返回",
|
contentDescription = "返回",
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
@ -182,94 +229,151 @@ private fun UserDetailTopBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户详情卡片
|
* 简洁的加载状态
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun UserProfileLoadingState() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
strokeWidth = 3.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "加载用户信息中...",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无边界用户信息区域
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun UserDetailCard(
|
private fun UserProfileSection(
|
||||||
user: User,
|
user: User,
|
||||||
onFollowClick: () -> Unit,
|
onFollowClick: () -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope?,
|
sharedTransitionScope: SharedTransitionScope?,
|
||||||
animatedContentScope: AnimatedContentScope?
|
animatedContentScope: AnimatedContentScope?,
|
||||||
|
sharedElementPrefix: String? = null
|
||||||
) {
|
) {
|
||||||
Card(
|
var isVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(24.dp)
|
||||||
shape = TangyuanShapes.CulturalCard,
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(
|
|
||||||
defaultElevation = 4.dp
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
// 头像和昵称区域
|
||||||
modifier = Modifier.padding(24.dp),
|
Row(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// 用户头像 - 支持共享元素动画
|
// 左侧头像 - 支持共享元素动画
|
||||||
ShimmerAsyncImage(
|
ShimmerAsyncImage(
|
||||||
imageUrl = "${TangyuanApplication.instance.bizDomain}images/${user.avatarGuid}.jpg",
|
imageUrl = "${TangyuanApplication.instance.bizDomain}images/${user.avatarGuid}.jpg",
|
||||||
contentDescription = "${user.nickName}的头像",
|
contentDescription = "${user.nickName}的头像",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(120.dp)
|
.size(80.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_${user.userId}"),
|
rememberSharedContentState(
|
||||||
|
key = sharedElementPrefix?.let { "${it}_user_avatar_${user.userId}" }
|
||||||
|
?: "user_avatar_${user.userId}"
|
||||||
|
),
|
||||||
animatedVisibilityScope = animatedContentScope,
|
animatedVisibilityScope = animatedContentScope,
|
||||||
boundsTransform = { _, _ ->
|
boundsTransform = { _, _ ->
|
||||||
tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
tween(
|
||||||
|
durationMillis = 400,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else mod
|
} else mod
|
||||||
},
|
}
|
||||||
|
.clip(CircleShape),
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.width(20.dp))
|
||||||
|
|
||||||
// 用户名
|
// 右侧昵称和信息
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
// 昵称 - 支持共享元素动画
|
||||||
Text(
|
Text(
|
||||||
text = user.nickName.withPanguSpacing(),
|
text = user.nickName.withPanguSpacing(),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center
|
modifier = if (sharedTransitionScope != null && animatedContentScope != null) {
|
||||||
)
|
with(sharedTransitionScope) {
|
||||||
|
Modifier.sharedElement(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
rememberSharedContentState(
|
||||||
|
key = sharedElementPrefix?.let { "${it}_user_name_${user.userId}" }
|
||||||
// 用户简介
|
?: "user_name_${user.userId}"
|
||||||
if (user.bio.isNotBlank()) {
|
),
|
||||||
Text(
|
animatedVisibilityScope = animatedContentScope,
|
||||||
text = user.bio.withPanguSpacing(),
|
boundsTransform = { _, _ ->
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
||||||
fontFamily = LiteraryFontFamily,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
lineHeight = 22.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
|
||||||
// 地区信息
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 地区和邮箱信息 - 右侧划入动画
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isVisible,
|
||||||
|
enter = slideInHorizontally(
|
||||||
|
initialOffsetX = { it },
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessMedium
|
||||||
|
)
|
||||||
|
) + fadeIn(
|
||||||
|
animationSpec = tween(400, delayMillis = 100)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
// 地区信息胶囊
|
||||||
if (user.isoRegionName.isNotBlank()) {
|
if (user.isoRegionName.isNotBlank()) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier.wrapContentWidth()
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.LocationOn,
|
imageVector = Icons.Outlined.LocationOn,
|
||||||
contentDescription = "地区",
|
contentDescription = "地区",
|
||||||
tint = MaterialTheme.colorScheme.tertiary,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
@ -280,34 +384,81 @@ private fun UserDetailCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 邮箱信息胶囊
|
||||||
|
if (user.email.isNotBlank()) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier.wrapContentWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Email,
|
||||||
|
contentDescription = "邮箱",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = user.email,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
// 关注按钮
|
// 用户签名
|
||||||
Button(
|
if (user.bio.isNotBlank()) {
|
||||||
onClick = onFollowClick,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(48.dp),
|
|
||||||
shape = MaterialTheme.shapes.small,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.PersonAdd,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = "关注",
|
text = user.bio.withPanguSpacing(),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
fontFamily = LiteraryFontFamily,
|
||||||
fontWeight = FontWeight.Medium
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
lineHeight = 22.sp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// // 关注按钮
|
||||||
|
// Button(
|
||||||
|
// onClick = onFollowClick,
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxWidth()
|
||||||
|
// .height(48.dp),
|
||||||
|
// shape = RoundedCornerShape(12.dp),
|
||||||
|
// colors = ButtonDefaults.buttonColors(
|
||||||
|
// containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
// )
|
||||||
|
// ) {
|
||||||
|
// Icon(
|
||||||
|
// imageVector = Icons.Outlined.PersonAdd,
|
||||||
|
// contentDescription = null,
|
||||||
|
// modifier = Modifier.size(18.dp)
|
||||||
|
// )
|
||||||
|
// Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
// Text(
|
||||||
|
// text = "关注",
|
||||||
|
// style = MaterialTheme.typography.labelLarge,
|
||||||
|
// fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
// fontWeight = FontWeight.Medium
|
||||||
|
// )
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,8 +467,7 @@ private fun UserDetailCard(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun UserStatsSection(
|
private fun UserStatsSection(
|
||||||
postsCount: Int,
|
postsCount: Int
|
||||||
user: User
|
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -362,6 +512,128 @@ private fun UserStatsSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 帖子区域标题
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PostsSectionHeader(postsCount: Int) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "帖子",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "($postsCount)",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 帖子加载状态
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PostsLoadingState() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "加载帖子中...",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空帖子状态
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun EmptyPostsState() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PostAdd,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "还没有发布过帖子",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "期待 TA 的第一个分享",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = LiteraryFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 底部到底提示
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun BottomIndicator() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.4f),
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "已经到底了",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = LiteraryFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "去发现更多精彩吧",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = LiteraryFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计项组件
|
* 统计项组件
|
||||||
*/
|
*/
|
||||||
@ -389,247 +661,27 @@ private fun StatItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 帖子列表区域
|
|
||||||
*/
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun PostsSection(
|
fun UserProfilePreview() {
|
||||||
posts: List<PostMetadata>,
|
val sampleUser = User(
|
||||||
isLoading: Boolean,
|
userId = 1,
|
||||||
onPostClick: (Int) -> Unit
|
nickName = "示例用户",
|
||||||
) {
|
avatarGuid = "sample_avatar",
|
||||||
Column(
|
bio = "这是一个示例用户的签名,用于展示用户详情卡片的样式。",
|
||||||
modifier = Modifier.padding(top = 16.dp)
|
email = "123@example.com",
|
||||||
) {
|
isoRegionName = "示例地区",
|
||||||
// 标题
|
phoneNumber = "+1234567890",
|
||||||
Row(
|
password = "password",
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "发布的帖子",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
UserProfileSection(
|
||||||
Text(
|
user = sampleUser,
|
||||||
text = "${posts.size}篇",
|
onFollowClick = {},
|
||||||
style = MaterialTheme.typography.bodySmall,
|
sharedTransitionScope = null,
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
animatedContentScope = null
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
// 加载状态
|
|
||||||
repeat(3) {
|
|
||||||
PostItemSkeleton()
|
|
||||||
}
|
|
||||||
} else if (posts.isEmpty()) {
|
|
||||||
// 空状态
|
|
||||||
EmptyPostsState()
|
|
||||||
} else {
|
|
||||||
// 帖子列表
|
|
||||||
posts.forEach { post ->
|
|
||||||
UserPostItem(
|
|
||||||
post = post,
|
|
||||||
onClick = { onPostClick(post.postId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户帖子项
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun UserPostItem(
|
|
||||||
post: PostMetadata,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
|
||||||
.clickable { onClick() },
|
|
||||||
shape = MaterialTheme.shapes.small,
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(
|
|
||||||
defaultElevation = 1.dp
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "帖子 #${post.postId}",
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = formatPostDate(post.postDateTime),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.ChevronRight,
|
|
||||||
contentDescription = "查看详情",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载骨架屏
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun PostItemSkeleton() {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(0.7f)
|
|
||||||
.height(16.dp)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
|
||||||
RoundedCornerShape(4.dp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(0.4f)
|
|
||||||
.height(12.dp)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
|
||||||
RoundedCornerShape(4.dp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 空状态
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun EmptyPostsState() {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(32.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Article,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = "还没有发布任何帖子",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
fontFamily = TangyuanGeneralFontFamily,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户详情加载卡片
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun UserDetailLoadingCard() {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
shape = TangyuanShapes.CulturalCard
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
// 头像占位
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(120.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// 用户名占位
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(120.dp)
|
|
||||||
.height(24.dp)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
|
||||||
RoundedCornerShape(4.dp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// 简介占位
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(200.dp)
|
|
||||||
.height(16.dp)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
|
|
||||||
RoundedCornerShape(4.dp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化帖子日期
|
|
||||||
*/
|
|
||||||
private fun formatPostDate(date: Date?): String {
|
|
||||||
return date?.let {
|
|
||||||
val formatter = SimpleDateFormat("MM月dd日", Locale.getDefault())
|
|
||||||
formatter.format(it)
|
|
||||||
} ?: "未知时间"
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,3 +132,99 @@ 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