feat: Implement user detail screen and refactor navigation
This commit introduces a dedicated user detail screen, enabling users to view profiles, and refactors the navigation flow, particularly for post and image details, to be more modular and robust.
**Key Changes:**
* **feat(UserDetailScreen):**
* Added a new `UserDetailScreen` to display user information, including their avatar, name, bio, and a list of their posts.
* Implemented a `UserDetailViewModel` to fetch user data and their associated posts from the repository.
* The screen is composed of several modular components: `UserDetailCard`, `UserStatsSection`, and `PostsSection`.
* Includes loading skeletons and an empty state for the user's post list.
* Integrated a pull-to-refresh mechanism to update user data.
* **feat(Navigation):**
* Added a new `UserDetail` route (`user_detail/{userId}`) to the navigation graph.
* Users can now navigate from a post card or post detail view to the author's `UserDetailScreen`.
* Clicking a post in the `UserDetailScreen` navigates to the corresponding `PostDetailScreen`.
* **refactor(Navigation & Post/Image Detail):**
* Decoupled the text and image detail views, removing the `PostDetailContainer` that previously managed mode switching with `AnimatedContent`.
* `PostDetailScreen` and `ImageDetailScreen` are now independent navigation destinations.
* Navigation from a post to its image view is now a direct navigation action to `ImageDetailScreen`, simplifying the logic.
* The route for `PostDetail` has been simplified to `post_detail/{postId}`, removing the `mode` parameter.
* **feat(Shared Element Transition):**
* Implemented a shared element transition for the user avatar, animating it from `PostCardItem` to `UserDetailScreen`.
* The avatar transition key is based on `user_avatar_{userId}` for consistent animations.
* **refactor(ImageDetailScreen):**
* Improved the shared element transition for images by setting `placeHolderSize` to `animatedSize` and disabling `renderInOverlayDuringTransition`, which can resolve certain animation artifacts.
* **refactor(Repository):**
* Added `getUserPosts(userId)` to the `UserRepository` to fetch all post metadata for a specific user.
This commit is contained in:
parent
e373670715
commit
39b5c3e40f
@ -23,6 +23,7 @@ import com.qingshuige.tangyuan.ui.screens.PostDetailScreen
|
|||||||
import com.qingshuige.tangyuan.ui.screens.ImageDetailScreen
|
import com.qingshuige.tangyuan.ui.screens.ImageDetailScreen
|
||||||
import com.qingshuige.tangyuan.ui.screens.TalkScreen
|
import com.qingshuige.tangyuan.ui.screens.TalkScreen
|
||||||
import com.qingshuige.tangyuan.ui.screens.LoginScreen
|
import com.qingshuige.tangyuan.ui.screens.LoginScreen
|
||||||
|
import com.qingshuige.tangyuan.ui.screens.UserDetailScreen
|
||||||
|
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -41,7 +42,7 @@ fun App() {
|
|||||||
navController.navigate(Screen.PostDetail.createRoute(postId))
|
navController.navigate(Screen.PostDetail.createRoute(postId))
|
||||||
},
|
},
|
||||||
onImageClick = { postId, imageIndex ->
|
onImageClick = { postId, imageIndex ->
|
||||||
navController.navigate(Screen.PostDetail.createRoute(postId, "image") + "?imageIndex=$imageIndex")
|
navController.navigate(Screen.ImageDetail.createRoute(postId, imageIndex))
|
||||||
},
|
},
|
||||||
sharedTransitionScope = this@SharedTransitionLayout,
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
animatedContentScope = this@composable
|
animatedContentScope = this@composable
|
||||||
@ -81,115 +82,85 @@ fun App() {
|
|||||||
LoginScreen(navController = navController)
|
LoginScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 帖子详情页 - 统一容器管理两种模式
|
// 帖子详情页
|
||||||
composable(
|
composable(
|
||||||
route = Screen.PostDetail.route + "?imageIndex={imageIndex}",
|
route = Screen.PostDetail.route,
|
||||||
arguments = listOf(
|
arguments = listOf(
|
||||||
navArgument("postId") { type = NavType.IntType },
|
navArgument("postId") { type = NavType.IntType }
|
||||||
navArgument("mode") { type = NavType.StringType; defaultValue = "text" },
|
|
||||||
navArgument("imageIndex") {
|
|
||||||
type = NavType.IntType
|
|
||||||
defaultValue = 0
|
|
||||||
nullable = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
val postId = backStackEntry.arguments?.getInt("postId") ?: 0
|
val postId = backStackEntry.arguments?.getInt("postId") ?: 0
|
||||||
val initialMode = backStackEntry.arguments?.getString("mode") ?: "text"
|
|
||||||
val imageIndex = backStackEntry.arguments?.getInt("imageIndex") ?: 0
|
|
||||||
|
|
||||||
PostDetailContainer(
|
PostDetailScreen(
|
||||||
postId = postId,
|
postId = postId,
|
||||||
initialMode = initialMode,
|
|
||||||
initialImageIndex = imageIndex,
|
|
||||||
onBackClick = { navController.popBackStack() },
|
onBackClick = { navController.popBackStack() },
|
||||||
onAuthorClick = { authorId ->
|
onAuthorClick = { authorId ->
|
||||||
// TODO: 导航到用户详情页
|
navController.navigate(Screen.UserDetail.createRoute(authorId))
|
||||||
|
},
|
||||||
|
onImageClick = { postId, imageIndex ->
|
||||||
|
navController.navigate(Screen.ImageDetail.createRoute(postId, imageIndex)) {
|
||||||
|
popUpTo(Screen.PostDetail.createRoute(postId)) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
sharedTransitionScope = this@SharedTransitionLayout,
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
animatedContentScope = this@composable
|
animatedContentScope = this@composable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// 图片详情页
|
||||||
* 帖子详情容器 - 统一管理文字和图片两种模式
|
composable(
|
||||||
* 保持与PostCard的共享元素动画,同时支持内部模式切换动画
|
route = Screen.ImageDetail.route,
|
||||||
*/
|
arguments = listOf(
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
navArgument("postId") { type = NavType.IntType },
|
||||||
@Composable
|
navArgument("imageIndex") { type = NavType.IntType }
|
||||||
fun PostDetailContainer(
|
)
|
||||||
postId: Int,
|
) { backStackEntry ->
|
||||||
initialMode: String,
|
val postId = backStackEntry.arguments?.getInt("postId") ?: 0
|
||||||
initialImageIndex: Int = 0,
|
val imageIndex = backStackEntry.arguments?.getInt("imageIndex") ?: 0
|
||||||
onBackClick: () -> Unit,
|
|
||||||
onAuthorClick: (Int) -> Unit,
|
|
||||||
sharedTransitionScope: SharedTransitionScope,
|
|
||||||
animatedContentScope: AnimatedContentScope
|
|
||||||
) {
|
|
||||||
// 本地状态管理模式切换
|
|
||||||
var currentMode by remember { mutableStateOf(initialMode) }
|
|
||||||
var imageIndex by remember { mutableIntStateOf(initialImageIndex) }
|
|
||||||
|
|
||||||
// 为模式切换创建内部AnimatedContent
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = currentMode,
|
|
||||||
transitionSpec = {
|
|
||||||
// 使用更流畅的动画让切换更自然
|
|
||||||
when {
|
|
||||||
targetState == "image" && initialState == "text" -> {
|
|
||||||
slideInVertically(
|
|
||||||
initialOffsetY = { it },
|
|
||||||
animationSpec = tween(500, easing = FastOutSlowInEasing)
|
|
||||||
) togetherWith slideOutVertically(
|
|
||||||
targetOffsetY = { -it },
|
|
||||||
animationSpec = tween(500, easing = FastOutSlowInEasing)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
targetState == "text" && initialState == "image" -> {
|
|
||||||
slideInVertically(
|
|
||||||
initialOffsetY = { -it },
|
|
||||||
animationSpec = tween(500, easing = FastOutSlowInEasing)
|
|
||||||
) togetherWith slideOutVertically(
|
|
||||||
targetOffsetY = { it },
|
|
||||||
animationSpec = tween(500, easing = FastOutSlowInEasing)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
fadeIn(animationSpec = tween(400)) togetherWith
|
|
||||||
fadeOut(animationSpec = tween(400))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = "detail_mode_switch"
|
|
||||||
) { mode ->
|
|
||||||
when (mode) {
|
|
||||||
"image" -> {
|
|
||||||
ImageDetailScreen(
|
ImageDetailScreen(
|
||||||
postId = postId,
|
postId = postId,
|
||||||
initialImageIndex = imageIndex,
|
initialImageIndex = imageIndex,
|
||||||
onBackClick = onBackClick,
|
onBackClick = { navController.popBackStack() },
|
||||||
onAuthorClick = onAuthorClick,
|
onAuthorClick = { authorId ->
|
||||||
onSwitchToTextMode = {
|
navController.navigate(Screen.UserDetail.createRoute(authorId))
|
||||||
currentMode = "text"
|
|
||||||
},
|
},
|
||||||
sharedTransitionScope = if (mode == initialMode) sharedTransitionScope else null,
|
onSwitchToTextMode = {
|
||||||
animatedContentScope = if (mode == initialMode) animatedContentScope else null
|
navController.navigate(Screen.PostDetail.createRoute(postId)) {
|
||||||
|
popUpTo(Screen.ImageDetail.createRoute(postId, imageIndex)) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
|
animatedContentScope = this@composable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
PostDetailScreen(
|
// 用户详情页
|
||||||
postId = postId,
|
composable(
|
||||||
onBackClick = onBackClick,
|
route = Screen.UserDetail.route,
|
||||||
onAuthorClick = onAuthorClick,
|
arguments = listOf(
|
||||||
onImageClick = { _, selectedImageIndex ->
|
navArgument("userId") { type = NavType.IntType }
|
||||||
imageIndex = selectedImageIndex
|
)
|
||||||
currentMode = "image"
|
) { backStackEntry ->
|
||||||
|
val userId = backStackEntry.arguments?.getInt("userId") ?: 0
|
||||||
|
|
||||||
|
UserDetailScreen(
|
||||||
|
userId = userId,
|
||||||
|
onBackClick = { navController.popBackStack() },
|
||||||
|
onPostClick = { postId ->
|
||||||
|
navController.navigate(Screen.PostDetail.createRoute(postId))
|
||||||
},
|
},
|
||||||
sharedTransitionScope = if (mode == initialMode) sharedTransitionScope else null,
|
onFollowClick = {
|
||||||
animatedContentScope = if (mode == initialMode) animatedContentScope else null
|
// TODO: 实现关注功能
|
||||||
|
},
|
||||||
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
|
animatedContentScope = this@composable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,7 +181,8 @@ fun MainFlow(
|
|||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
|
||||||
val bottomBarScreens = listOf(Screen.Talk, Screen.Topic, Screen.Message, Screen.User)
|
val bottomBarScreens = listOf(Screen.Talk, Screen.Topic, Screen.Message, Screen.User)
|
||||||
val currentScreen = bottomBarScreens.find { it.route == currentDestination?.route } ?: Screen.Talk
|
val currentScreen =
|
||||||
|
bottomBarScreens.find { it.route == currentDestination?.route } ?: Screen.Talk
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -220,8 +192,8 @@ fun MainFlow(
|
|||||||
avatarUrl = "https://dogeoss.grtsinry43.com/img/author.jpeg",
|
avatarUrl = "https://dogeoss.grtsinry43.com/img/author.jpeg",
|
||||||
pageLevel = PageLevel.PRIMARY,
|
pageLevel = PageLevel.PRIMARY,
|
||||||
onAvatarClick = onLoginClick,
|
onAvatarClick = onLoginClick,
|
||||||
onAnnouncementClick = { /* 公告点击事件 */ },
|
onAnnouncementClick = {/* 公告点击事件 */ },
|
||||||
onPostClick = { /* 发表点击事件 */ }
|
onPostClick = {/* 发表点击事件 */ }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
|||||||
@ -6,10 +6,13 @@ sealed class Screen(val route: String, val title: String) {
|
|||||||
object Topic : Screen("topic", "侃一侃")
|
object Topic : Screen("topic", "侃一侃")
|
||||||
object Message : Screen("message", "消息")
|
object Message : Screen("message", "消息")
|
||||||
object User : Screen("settings", "我的")
|
object User : Screen("settings", "我的")
|
||||||
object PostDetail : Screen("post_detail/{postId}/{mode}", "帖子详情") {
|
object PostDetail : Screen("post_detail/{postId}", "帖子详情") {
|
||||||
fun createRoute(postId: Int, mode: String = "text") = "post_detail/$postId/$mode"
|
fun createRoute(postId: Int) = "post_detail/$postId"
|
||||||
}
|
}
|
||||||
object ImageDetail : Screen("image_detail/{postId}/{imageIndex}", "图片详情") {
|
object ImageDetail : Screen("image_detail/{postId}/{imageIndex}", "图片详情") {
|
||||||
fun createRoute(postId: Int, imageIndex: Int) = "image_detail/$postId/$imageIndex"
|
fun createRoute(postId: Int, imageIndex: Int) = "image_detail/$postId/$imageIndex"
|
||||||
}
|
}
|
||||||
|
object UserDetail : Screen("user_detail/{userId}", "用户详情") {
|
||||||
|
fun createRoute(userId: Int) = "user_detail/$userId"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -62,4 +62,14 @@ class UserRepository @Inject constructor(
|
|||||||
throw Exception("Search failed: ${response.message()}")
|
throw Exception("Search failed: ${response.message()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUserPosts(userId: Int): Flow<List<com.qingshuige.tangyuan.model.PostMetadata>> = flow {
|
||||||
|
val response = apiInterface.getMetadatasByUserID(userId).awaitResponse()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body()?.let { emit(it) }
|
||||||
|
?: emit(emptyList())
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to get user posts: ${response.message()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -174,7 +174,9 @@ fun PostCardItem(
|
|||||||
PostCardHeader(
|
PostCardHeader(
|
||||||
postCard = postCard,
|
postCard = postCard,
|
||||||
onAuthorClick = onAuthorClick,
|
onAuthorClick = onAuthorClick,
|
||||||
onMoreClick = onMoreClick
|
onMoreClick = onMoreClick,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedContentScope = animatedContentScope
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
@ -216,11 +218,14 @@ fun PostCardItem(
|
|||||||
/**
|
/**
|
||||||
* 文章卡片头部 - 作者信息
|
* 文章卡片头部 - 作者信息
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PostCardHeader(
|
private fun PostCardHeader(
|
||||||
postCard: PostCard,
|
postCard: PostCard,
|
||||||
onAuthorClick: (Int) -> Unit,
|
onAuthorClick: (Int) -> Unit,
|
||||||
onMoreClick: (Int) -> Unit
|
onMoreClick: (Int) -> Unit,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedContentScope: AnimatedContentScope? = null
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@ -232,7 +237,20 @@ private fun PostCardHeader(
|
|||||||
contentDescription = "${postCard.authorName}的头像",
|
contentDescription = "${postCard.authorName}的头像",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.clip(CircleShape),
|
.clip(CircleShape)
|
||||||
|
.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
|
||||||
|
},
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
onClick = { onAuthorClick(postCard.authorId) }
|
onClick = { onAuthorClick(postCard.authorId) }
|
||||||
)
|
)
|
||||||
|
|||||||
@ -283,7 +283,9 @@ private fun ZoomableImage(
|
|||||||
animatedVisibilityScope = animatedContentScope,
|
animatedVisibilityScope = animatedContentScope,
|
||||||
boundsTransform = { _, _ ->
|
boundsTransform = { _, _ ->
|
||||||
tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
||||||
}
|
},
|
||||||
|
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize,
|
||||||
|
renderInOverlayDuringTransition = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else mod
|
} else mod
|
||||||
|
|||||||
@ -0,0 +1,635 @@
|
|||||||
|
package com.qingshuige.tangyuan.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material.icons.outlined.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.qingshuige.tangyuan.TangyuanApplication
|
||||||
|
import com.qingshuige.tangyuan.model.PostMetadata
|
||||||
|
import com.qingshuige.tangyuan.model.User
|
||||||
|
import com.qingshuige.tangyuan.ui.components.ShimmerAsyncImage
|
||||||
|
import com.qingshuige.tangyuan.ui.theme.LiteraryFontFamily
|
||||||
|
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.utils.withPanguSpacing
|
||||||
|
import com.qingshuige.tangyuan.viewmodel.UserDetailViewModel
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户详情页
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
fun UserDetailScreen(
|
||||||
|
userId: Int,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onPostClick: (Int) -> Unit = {},
|
||||||
|
onFollowClick: () -> Unit = {},
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedContentScope: AnimatedContentScope? = null,
|
||||||
|
viewModel: UserDetailViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val user by viewModel.user.collectAsState()
|
||||||
|
val userPosts by viewModel.userPosts.collectAsState()
|
||||||
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
|
val isPostsLoading by viewModel.isPostsLoading.collectAsState()
|
||||||
|
val errorMessage by viewModel.errorMessage.collectAsState()
|
||||||
|
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
LaunchedEffect(userId) {
|
||||||
|
viewModel.loadUserDetails(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误提示
|
||||||
|
errorMessage?.let { message ->
|
||||||
|
LaunchedEffect(message) {
|
||||||
|
// TODO: 显示错误提示
|
||||||
|
viewModel.clearError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
isRefreshing = true
|
||||||
|
viewModel.refreshUserData(userId)
|
||||||
|
isRefreshing = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(bottom = 100.dp)
|
||||||
|
) {
|
||||||
|
// 顶部导航栏
|
||||||
|
item {
|
||||||
|
UserDetailTopBar(
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
userName = user?.nickName ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息卡片
|
||||||
|
item {
|
||||||
|
if (isLoading) {
|
||||||
|
UserDetailLoadingCard()
|
||||||
|
} else {
|
||||||
|
user?.let { userInfo ->
|
||||||
|
UserDetailCard(
|
||||||
|
user = userInfo,
|
||||||
|
onFollowClick = onFollowClick,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedContentScope = animatedContentScope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
item {
|
||||||
|
user?.let { userInfo ->
|
||||||
|
UserStatsSection(
|
||||||
|
postsCount = userPosts.size,
|
||||||
|
user = userInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户帖子列表
|
||||||
|
item {
|
||||||
|
PostsSection(
|
||||||
|
posts = userPosts,
|
||||||
|
isLoading = isPostsLoading,
|
||||||
|
onPostClick = onPostClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部导航栏
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun UserDetailTopBar(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
userName: String
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = userName.withPanguSpacing(),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
|
contentDescription = "返回",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { /* TODO: 更多操作 */ }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = "更多",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户详情卡片
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun UserDetailCard(
|
||||||
|
user: User,
|
||||||
|
onFollowClick: () -> Unit,
|
||||||
|
sharedTransitionScope: SharedTransitionScope?,
|
||||||
|
animatedContentScope: AnimatedContentScope?
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
shape = TangyuanShapes.CulturalCard,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(
|
||||||
|
defaultElevation = 4.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// 用户头像 - 支持共享元素动画
|
||||||
|
ShimmerAsyncImage(
|
||||||
|
imageUrl = "${TangyuanApplication.instance.bizDomain}images/${user.avatarGuid}.jpg",
|
||||||
|
contentDescription = "${user.nickName}的头像",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(120.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.let { mod ->
|
||||||
|
if (sharedTransitionScope != null && animatedContentScope != null) {
|
||||||
|
with(sharedTransitionScope) {
|
||||||
|
mod.sharedElement(
|
||||||
|
rememberSharedContentState(key = "user_avatar_${user.userId}"),
|
||||||
|
animatedVisibilityScope = animatedContentScope,
|
||||||
|
boundsTransform = { _, _ ->
|
||||||
|
tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else mod
|
||||||
|
},
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// 用户名
|
||||||
|
Text(
|
||||||
|
text = user.nickName.withPanguSpacing(),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 用户简介
|
||||||
|
if (user.bio.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = user.bio.withPanguSpacing(),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontFamily = LiteraryFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
lineHeight = 22.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地区信息
|
||||||
|
if (user.isoRegionName.isNotBlank()) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.LocationOn,
|
||||||
|
contentDescription = "地区",
|
||||||
|
tint = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = user.isoRegionName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
// 关注按钮
|
||||||
|
Button(
|
||||||
|
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 = "关注",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计信息区域
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun UserStatsSection(
|
||||||
|
postsCount: Int,
|
||||||
|
user: User
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
StatItem(
|
||||||
|
label = "帖子",
|
||||||
|
value = postsCount.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier.height(40.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
|
||||||
|
StatItem(
|
||||||
|
label = "关注",
|
||||||
|
value = "0" // TODO: 从API获取关注数
|
||||||
|
)
|
||||||
|
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier.height(40.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
|
||||||
|
StatItem(
|
||||||
|
label = "粉丝",
|
||||||
|
value = "0" // TODO: 从API获取粉丝数
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计项组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun StatItem(
|
||||||
|
label: String,
|
||||||
|
value: String
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = TangyuanTypography.numberMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 帖子列表区域
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PostsSection(
|
||||||
|
posts: List<PostMetadata>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onPostClick: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
|
) {
|
||||||
|
// 标题
|
||||||
|
Row(
|
||||||
|
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))
|
||||||
|
Text(
|
||||||
|
text = "${posts.size}篇",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = TangyuanGeneralFontFamily,
|
||||||
|
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)
|
||||||
|
} ?: "未知时间"
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
package com.qingshuige.tangyuan.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.qingshuige.tangyuan.model.PostMetadata
|
||||||
|
import com.qingshuige.tangyuan.model.User
|
||||||
|
import com.qingshuige.tangyuan.repository.UserRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class UserDetailViewModel @Inject constructor(
|
||||||
|
private val userRepository: UserRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
// 用户信息状态
|
||||||
|
private val _user = MutableStateFlow<User?>(null)
|
||||||
|
val user: StateFlow<User?> = _user.asStateFlow()
|
||||||
|
|
||||||
|
// 用户帖子列表状态
|
||||||
|
private val _userPosts = MutableStateFlow<List<PostMetadata>>(emptyList())
|
||||||
|
val userPosts: StateFlow<List<PostMetadata>> = _userPosts.asStateFlow()
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
private val _isLoading = MutableStateFlow(false)
|
||||||
|
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||||
|
|
||||||
|
// 错误状态
|
||||||
|
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||||
|
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
|
||||||
|
|
||||||
|
// 帖子加载状态
|
||||||
|
private val _isPostsLoading = MutableStateFlow(false)
|
||||||
|
val isPostsLoading: StateFlow<Boolean> = _isPostsLoading.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载用户详细信息
|
||||||
|
*/
|
||||||
|
fun loadUserDetails(userId: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
_errorMessage.value = null
|
||||||
|
|
||||||
|
userRepository.getUserById(userId)
|
||||||
|
.catch { e ->
|
||||||
|
_errorMessage.value = e.message ?: "获取用户信息失败"
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
.collect { userInfo ->
|
||||||
|
_user.value = userInfo
|
||||||
|
_isLoading.value = false
|
||||||
|
// 获取用户信息成功后,加载用户的帖子
|
||||||
|
loadUserPosts(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载用户的帖子列表
|
||||||
|
*/
|
||||||
|
private fun loadUserPosts(userId: Int) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isPostsLoading.value = true
|
||||||
|
|
||||||
|
userRepository.getUserPosts(userId)
|
||||||
|
.catch { e ->
|
||||||
|
// 帖子加载失败不影响用户信息显示
|
||||||
|
_isPostsLoading.value = false
|
||||||
|
}
|
||||||
|
.collect { posts ->
|
||||||
|
_userPosts.value = posts
|
||||||
|
_isPostsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新用户数据
|
||||||
|
*/
|
||||||
|
fun refreshUserData(userId: Int) {
|
||||||
|
loadUserDetails(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除错误消息
|
||||||
|
*/
|
||||||
|
fun clearError() {
|
||||||
|
_errorMessage.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
fun updateUserInfo(userId: Int, updatedUser: User) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_isLoading.value = true
|
||||||
|
_errorMessage.value = null
|
||||||
|
|
||||||
|
userRepository.updateUser(userId, updatedUser)
|
||||||
|
.catch { e ->
|
||||||
|
_errorMessage.value = e.message ?: "更新用户信息失败"
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
.collect { success ->
|
||||||
|
if (success) {
|
||||||
|
_user.value = updatedUser
|
||||||
|
}
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user