账户编辑、评论点赞功能优化和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.SharedTransitionLayout
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
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.WindowInsets
import androidx.compose.foundation.layout.navigationBars
@@ -364,10 +367,40 @@ fun NavigationController(
composable(
route = NavigationRoute.AccountEdit.route,
enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 0))
// iOS风格从底部向上滑入
slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
) + fadeIn(
animationSpec = tween(durationMillis = 300)
)
},
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()

View File

@@ -28,6 +28,8 @@ object AccountEditViewModel : ViewModel() {
profile = it
name = it.nickName
bio = it.bio
// 清除之前裁剪的图片
croppedBitmap = null
if (updateTrtcProfile) {
TrtcHelper.updateTrtcProfile(
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) {
val newAvatar = croppedBitmap?.let {
@@ -44,12 +55,16 @@ object AccountEditViewModel : ViewModel() {
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
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(
avatar = newAvatar,
banner = null,
nickName = newName,
bio = bio
bio = cleanBio
)
// 刷新用户资料
reloadProfile()

View File

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

View File

@@ -152,38 +152,96 @@ class CommentsViewModel(
_commentsFlow.value = updatedPagingData
}
// 用于防止重复点赞的状态集合
private val _pendingLikeOperations = mutableSetOf<Int>()
/**
* 点赞评论
* 点赞评论 - 使用乐观更新策略
*/
suspend fun likeComment(commentId: Int) {
// 防止重复操作
if (_pendingLikeOperations.contains(commentId)) {
return
}
_pendingLikeOperations.add(commentId)
try {
// 乐观更新先更新UI状态
val previousState = getCurrentCommentLikeState(commentId)
updateCommentLikeState(commentId, true)
// 然后调用API
commentService.likeComment(commentId)
// 更新addCommentList
if (updateHighlightCommentLike(commentId, true)) {
return
}
if (updateAddedCommentLike(commentId, true)) {
return
}
updateCommentLike(commentId, true)
} catch (e: Exception) {
e.printStackTrace()
// 如果API调用失败回滚UI状态
updateCommentLikeState(commentId, false)
} finally {
_pendingLikeOperations.remove(commentId)
}
}
// 取消点赞评论
/**
* 取消点赞评论 - 使用乐观更新策略
*/
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
}
// 更新添加的评论点赞状态
if (updateAddedCommentLike(commentId, false)) {
if (updateAddedCommentLike(commentId, isLike)) {
return
}
// 更新评论点赞状态
updateCommentLike(commentId, false)
updateCommentLike(commentId, isLike)
}
suspend fun createComment(

View File

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