账户编辑、评论点赞功能优化和UI调整

**功能优化:**

*   **账户编辑:**
    *   昵称和个人简介输入时自动去除换行符。
    *   修复了进入编辑页面时可能未正确加载或重置用户资料的问题。
    *   保存资料时,确保昵称和个人简介中的换行符被移除。
    *   清除裁剪的头像图片,避免重复使用。
*   **评论点赞/取消点赞:**
    *   引入乐观更新策略,提升用户体验,点赞/取消点赞操作会立即反映在UI上,然后进行后台API调用。
    *   增加防抖机制,防止用户快速重复点击点赞/取消点赞按钮导致多次API请求。

**UI调整:**

*   **账户编辑页面:**
    *   页面切换动画调整为iOS风格的底部滑入滑出效果。
This commit is contained in:
2025-09-03 14:21:13 +08:00
parent 16f95782f8
commit 79547de2db
5 changed files with 142 additions and 25 deletions

View File

@@ -6,10 +6,13 @@ import ModificationListScreen
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
@@ -364,10 +367,40 @@ fun NavigationController(
composable( composable(
route = NavigationRoute.AccountEdit.route, route = NavigationRoute.AccountEdit.route,
enterTransition = { enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0)) // iOS风格从底部向上滑入
slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
) + fadeIn(
animationSpec = tween(durationMillis = 300)
)
}, },
exitTransition = { exitTransition = {
fadeOut(animationSpec = tween(durationMillis = 0)) // iOS风格向底部滑出
slideOutVertically(
targetOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
) + fadeOut(
animationSpec = tween(durationMillis = 300)
)
},
popEnterTransition = {
// 返回时从底部滑入
slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
) + fadeIn(
animationSpec = tween(durationMillis = 300)
)
},
popExitTransition = {
// 返回时向底部滑出
slideOutVertically(
targetOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
) + fadeOut(
animationSpec = tween(durationMillis = 300)
)
} }
) { ) {
AccountEditScreen2() AccountEditScreen2()

View File

@@ -28,6 +28,8 @@ object AccountEditViewModel : ViewModel() {
profile = it profile = it
name = it.nickName name = it.nickName
bio = it.bio bio = it.bio
// 清除之前裁剪的图片
croppedBitmap = null
if (updateTrtcProfile) { if (updateTrtcProfile) {
TrtcHelper.updateTrtcProfile( TrtcHelper.updateTrtcProfile(
it.nickName, it.nickName,
@@ -37,6 +39,15 @@ object AccountEditViewModel : ViewModel() {
} }
} }
fun resetToOriginalData() {
profile?.let {
name = it.nickName
bio = it.bio
// 清除之前裁剪的图片
croppedBitmap = null
}
}
suspend fun updateUserProfile(context: Context) { suspend fun updateUserProfile(context: Context) {
val newAvatar = croppedBitmap?.let { val newAvatar = croppedBitmap?.let {
@@ -44,12 +55,16 @@ object AccountEditViewModel : ViewModel() {
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream()) it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
UploadImage(file, "avatar.jpg", "", "jpg") UploadImage(file, "avatar.jpg", "", "jpg")
} }
val newName = if (name == profile?.nickName) null else name // 去除换行符,确保昵称和个人简介不包含换行
val cleanName = name.trim().replace("\n", "").replace("\r", "")
val cleanBio = bio.trim().replace("\n", "").replace("\r", "")
val newName = if (cleanName == profile?.nickName) null else cleanName
accountService.updateProfile( accountService.updateProfile(
avatar = newAvatar, avatar = newAvatar,
banner = null, banner = null,
nickName = newName, nickName = newName,
bio = bio bio = cleanBio
) )
// 刷新用户资料 // 刷新用户资料
reloadProfile() reloadProfile()

View File

@@ -55,11 +55,13 @@ fun AccountEditScreen2() {
var usernameError by remember { mutableStateOf<String?>(null) } var usernameError by remember { mutableStateOf<String?>(null) }
var bioError by remember { mutableStateOf<String?>(null) } var bioError by remember { mutableStateOf<String?>(null) }
fun onNicknameChange(value: String) { fun onNicknameChange(value: String) {
model.name = value // 去除换行符,确保昵称不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "")
model.name = cleanValue
usernameError = when { usernameError = when {
value.trim().isEmpty() -> "昵称不能为空" cleanValue.trim().isEmpty() -> "昵称不能为空"
value.length < 3 -> "昵称长度不能小于3" cleanValue.length < 3 -> "昵称长度不能小于3"
value.length > 20 -> "昵称长度不能大于20" cleanValue.length > 20 -> "昵称长度不能大于20"
else -> null else -> null
} }
} }
@@ -67,9 +69,11 @@ fun AccountEditScreen2() {
val appColors = LocalAppTheme.current val appColors = LocalAppTheme.current
fun onBioChange(value: String) { fun onBioChange(value: String) {
model.bio = value // 去除换行符,确保个人简介不包含换行
val cleanValue = value.replace("\n", "").replace("\r", "")
model.bio = cleanValue
bioError = when { bioError = when {
value.length > 100 -> "个人简介长度不能大于24" cleanValue.length > 100 -> "个人简介长度不能大于100"
else -> null else -> null
} }
} }
@@ -79,10 +83,13 @@ fun AccountEditScreen2() {
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// 先初始化显示当前资料,避免重置画面
if (model.profile == null) { if (model.profile == null) {
model.reloadProfile() model.reloadProfile()
} else {
// 重置编辑状态为原始资料数据
model.resetToOriginalData()
} }
} }
StatusBarMaskLayout( StatusBarMaskLayout(
modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp), modifier = Modifier.background(color = appColors.background).padding(horizontal = 16.dp),

View File

@@ -152,38 +152,96 @@ class CommentsViewModel(
_commentsFlow.value = updatedPagingData _commentsFlow.value = updatedPagingData
} }
// 用于防止重复点赞的状态集合
private val _pendingLikeOperations = mutableSetOf<Int>()
/** /**
* 点赞评论 * 点赞评论 - 使用乐观更新策略
*/ */
suspend fun likeComment(commentId: Int) { suspend fun likeComment(commentId: Int) {
// 防止重复操作
if (_pendingLikeOperations.contains(commentId)) {
return
}
_pendingLikeOperations.add(commentId)
try { try {
// 乐观更新先更新UI状态
val previousState = getCurrentCommentLikeState(commentId)
updateCommentLikeState(commentId, true)
// 然后调用API
commentService.likeComment(commentId) commentService.likeComment(commentId)
// 更新addCommentList
if (updateHighlightCommentLike(commentId, true)) {
return
}
if (updateAddedCommentLike(commentId, true)) {
return
}
updateCommentLike(commentId, true)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
// 如果API调用失败回滚UI状态
updateCommentLikeState(commentId, false)
} finally {
_pendingLikeOperations.remove(commentId)
} }
} }
// 取消点赞评论 /**
* 取消点赞评论 - 使用乐观更新策略
*/
suspend fun unlikeComment(commentId: Int) { suspend fun unlikeComment(commentId: Int) {
commentService.dislikeComment(commentId) // 防止重复操作
if (_pendingLikeOperations.contains(commentId)) {
return
}
_pendingLikeOperations.add(commentId)
try {
// 乐观更新先更新UI状态
val previousState = getCurrentCommentLikeState(commentId)
updateCommentLikeState(commentId, false)
// 然后调用API
commentService.dislikeComment(commentId)
} catch (e: Exception) {
e.printStackTrace()
// 如果API调用失败回滚UI状态
updateCommentLikeState(commentId, true)
} finally {
_pendingLikeOperations.remove(commentId)
}
}
/**
* 获取当前评论的点赞状态
*/
private fun getCurrentCommentLikeState(commentId: Int): Boolean {
// 检查高亮评论
highlightComment?.let { comment ->
if (comment.id == commentId) {
return comment.liked
}
comment.reply.find { it.id == commentId }?.let { return it.liked }
}
// 检查添加的评论列表
addedCommentList.find { it.id == commentId }?.let { return it.liked }
// 检查分页数据(这里简化处理,实际项目中可能需要更复杂的逻辑)
return false
}
/**
* 统一的评论点赞状态更新方法
*/
private fun updateCommentLikeState(commentId: Int, isLike: Boolean) {
// 更新高亮评论点赞状态 // 更新高亮评论点赞状态
if (updateHighlightCommentLike(commentId, false)) { if (updateHighlightCommentLike(commentId, isLike)) {
return return
} }
// 更新添加的评论点赞状态 // 更新添加的评论点赞状态
if (updateAddedCommentLike(commentId, false)) { if (updateAddedCommentLike(commentId, isLike)) {
return return
} }
// 更新评论点赞状态 // 更新评论点赞状态
updateCommentLike(commentId, false) updateCommentLike(commentId, isLike)
} }
suspend fun createComment( suspend fun createComment(

View File

@@ -209,6 +209,7 @@ fun PostScreen(
showCommentMenu = false showCommentMenu = false
} }
contextComment?.let { contextComment?.let {
// 防抖机制已在ViewModel中实现
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
if (it.liked) { if (it.liked) {
viewModel.unlikeComment(it.id) viewModel.unlikeComment(it.id)
@@ -523,6 +524,7 @@ fun CommentContent(
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route) navController.navigate(NavigationRoute.Login.route)
} else { } else {
// 使用防抖机制避免重复点击
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
if (comment.liked) { if (comment.liked) {
viewModel.unlikeComment(comment.id) viewModel.unlikeComment(comment.id)
@@ -577,6 +579,7 @@ fun CommentContent(
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route) navController.navigate(NavigationRoute.Login.route)
} else { } else {
// 防抖机制已在ViewModel中实现
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
if (comment.liked) { if (comment.liked) {
viewModel.unlikeComment(comment.id) viewModel.unlikeComment(comment.id)
@@ -630,6 +633,7 @@ fun CommentContent(
if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) { if (GuestLoginCheckOut.needLogin(GuestLoginCheckOutScene.LIKE_MOMENT)) {
navController.navigate(NavigationRoute.Login.route) navController.navigate(NavigationRoute.Login.route)
} else { } else {
// 防抖机制已在ViewModel中实现
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
if (comment.liked) { if (comment.liked) {
viewModel.unlikeComment(comment.id) viewModel.unlikeComment(comment.id)