grtsinry43 0f5bfe6aec
feat: Implement "Create Post" screen with image upload
This commit introduces a new "Create Post" screen, enabling users to compose and publish posts with text and up to three images. The implementation includes a dedicated ViewModel, robust state management, and an asynchronous image upload flow.

**Key Changes:**

*   **feat(Create Post Screen):**
    *   Added `CreatePostScreen.kt`, a new Composable screen for creating posts.
    *   The UI includes a text input field, selectors for "Section" and "Category," and an image picker/preview area.
    *   Implemented `SelectionBottomSheet` as a reusable component for picking sections and categories from a modal bottom sheet.
    *   The "Publish" button is dynamically enabled based on content validation and image upload status.
    *   Error messages are displayed to the user via a snackbar-like component.

*   **feat(CreatePostViewModel):**
    *   Created a new `CreatePostViewModel` to manage the logic and state (`CreatePostState`) of the creation process.
    *   Fetches post categories on initialization.
    *   Handles image selection and manages an asynchronous upload queue. Each selected image is immediately uploaded to the backend via `MediaRepository`.
    *   The ViewModel tracks the upload progress for each image and updates the UI accordingly.
    *   The `createPost` function validates all inputs, compiles the post data (including uploaded image UUIDs), and sends it to the repository.

*   **feat(Navigation):**
    *   Added a `CreatePost` route to the navigation graph (`Screen.kt`).
    *   Integrated the new screen into `App.kt` with custom horizontal slide transitions.
    *   The publish button on the main screen's top bar now navigates to the `CreatePostScreen`.
    *   Upon successful post creation, the user is navigated back to the main screen.

*   **refactor(PostDetailViewModel):**
    *   Adjusted the `createComment` logic to correctly handle `parentCommentId`, sending `0L` instead of `null` when there is no parent comment.
2025-10-09 00:53:20 +08:00

329 lines
11 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.qingshuige.tangyuan.viewmodel
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.qingshuige.tangyuan.model.CommentCard
import com.qingshuige.tangyuan.model.CreateCommentDto
import com.qingshuige.tangyuan.model.PostCard
import com.qingshuige.tangyuan.model.PostDetailState
import com.qingshuige.tangyuan.network.TokenManager
import com.qingshuige.tangyuan.repository.PostDetailRepository
import com.qingshuige.tangyuan.utils.ImageSaveUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
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 java.util.Date
import javax.inject.Inject
@HiltViewModel
class PostDetailViewModel @Inject constructor(
private val postDetailRepository: PostDetailRepository,
@ApplicationContext private val context: Context
) : ViewModel() {
private val tokenManager = TokenManager()
private val _state = MutableStateFlow(PostDetailState())
val state: StateFlow<PostDetailState> = _state.asStateFlow()
private var currentPostId: Int = 0
private var currentUserId: Int = 0
/**
* 加载帖子详情和评论 - 分离加载,先加载帖子再加载评论
*/
fun loadPostDetail(postId: Int) {
currentPostId = postId
currentUserId = tokenManager.getUserIdFromToken() ?: 0
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
try {
// 先加载帖子详情立即更新UI
postDetailRepository.getPostCard(postId)
.catch { e ->
_state.value = _state.value.copy(
isLoading = false,
error = e.message ?: "加载帖子失败"
)
}
.collect { postCard ->
// 立即更新帖子数据,确保共享元素有目标
_state.value = _state.value.copy(
postCard = postCard,
isLoading = false
)
// 然后异步加载评论
loadComments(postId, currentUserId)
}
} catch (e: Exception) {
_state.value = _state.value.copy(
isLoading = false,
error = e.message ?: "加载失败"
)
}
}
}
/**
* 加载评论数据
*/
private fun loadComments(postId: Int, userId: Int = 0) {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true) // 这里的loading只影响评论区
postDetailRepository.getCommentCardsForPost(postId, userId)
.catch { e ->
_state.value = _state.value.copy(
isLoading = false,
error = e.message ?: "加载评论失败"
)
}
.collect { commentCards ->
_state.value = _state.value.copy(
isLoading = false,
comments = commentCards,
error = null
)
}
}
}
/**
* 刷新帖子详情
*/
fun refreshPostDetail() {
if (currentPostId == 0) return
viewModelScope.launch {
_state.value = _state.value.copy(isRefreshing = true)
postDetailRepository.refreshPostDetail(currentPostId, currentUserId)
.catch { e ->
_state.value = _state.value.copy(
isRefreshing = false,
error = e.message ?: "刷新失败"
)
}
.collect { (postCard, commentCards) ->
_state.value = _state.value.copy(
isRefreshing = false,
postCard = postCard,
comments = commentCards,
error = null
)
}
}
}
/**
* 发布新评论
*/
fun createComment(content: String, parentCommentId: Int = 0) {
if (currentPostId == 0 || content.isBlank()) return
viewModelScope.launch {
_state.value = _state.value.copy(
isCreatingComment = true,
commentError = null
)
val createCommentDto = CreateCommentDto(
postId = currentPostId.toLong(),
commentDateTime = Date(),
content = content,
parentCommentId = if (parentCommentId == 0) 0L else parentCommentId.toLong(),
userId = currentUserId.toLong()
)
postDetailRepository.createComment(createCommentDto)
.catch { e ->
_state.value = _state.value.copy(
isCreatingComment = false,
commentError = e.message ?: "评论发布失败"
)
}
.collect { message ->
_state.value = _state.value.copy(
isCreatingComment = false,
replyToComment = null,
commentError = null
)
// 评论发布成功后,刷新评论列表
refreshComments()
}
}
}
/**
* 删除评论
*/
fun deleteComment(commentId: Int) {
viewModelScope.launch {
postDetailRepository.deleteComment(commentId)
.catch { e ->
_state.value = _state.value.copy(
error = e.message ?: "删除评论失败"
)
}
.collect { success ->
if (success) {
// 删除成功后,从本地列表中移除该评论
val updatedComments = _state.value.comments.filter { comment ->
comment.commentId != commentId &&
comment.replies.none { it.commentId == commentId }
}.map { comment ->
comment.copy(
replies = comment.replies.filter { it.commentId != commentId }
)
}
_state.value = _state.value.copy(comments = updatedComments)
}
}
}
}
/**
* 设置回复目标评论
*/
fun setReplyToComment(commentCard: CommentCard?) {
_state.value = _state.value.copy(replyToComment = commentCard)
}
/**
* 刷新评论列表
*/
private fun refreshComments() {
if (currentPostId == 0) return
viewModelScope.launch {
postDetailRepository.getCommentCardsForPost(currentPostId, currentUserId)
.catch { e ->
_state.value = _state.value.copy(
error = e.message ?: "刷新评论失败"
)
}
.collect { commentCards ->
_state.value = _state.value.copy(comments = commentCards)
}
}
}
/**
* 加载更多评论
*/
fun loadMoreComments() {
// TODO: 实现分页加载评论
// 当前实现一次加载所有评论,后续可以根据需要实现分页
}
/**
* 展开/收起评论回复
*/
fun toggleReplies(commentId: Int) {
viewModelScope.launch {
val updatedComments = _state.value.comments.map { comment ->
if (comment.commentId == commentId) {
if (comment.replies.isEmpty() && comment.replyCount > 0) {
// 加载回复
loadRepliesForComment(commentId)
comment
} else {
// 切换显示状态(这里简单处理,实际可能需要添加展开状态字段)
comment
}
} else {
comment
}
}
_state.value = _state.value.copy(comments = updatedComments)
}
}
/**
* 加载特定评论的回复
*/
private fun loadRepliesForComment(commentId: Int) {
viewModelScope.launch {
postDetailRepository.getReplyCardsForComment(commentId, currentUserId)
.catch { e ->
_state.value = _state.value.copy(
error = e.message ?: "加载回复失败"
)
}
.collect { replyCards ->
val updatedComments = _state.value.comments.map { comment ->
if (comment.commentId == commentId) {
comment.copy(replies = replyCards)
} else {
comment
}
}
_state.value = _state.value.copy(comments = updatedComments)
}
}
}
/**
* 清除错误状态
*/
fun clearError() {
_state.value = _state.value.copy(error = null)
}
/**
* 清除评论错误状态
*/
fun clearCommentError() {
_state.value = _state.value.copy(commentError = null)
}
/**
* 重置状态
*/
fun resetState() {
_state.value = PostDetailState()
currentPostId = 0
currentUserId = 0
}
/**
* 保存当前图片到本地
*/
fun saveCurrentImage(imageUrl: String) {
viewModelScope.launch {
try {
val result = ImageSaveUtils.saveImageToGallery(context, imageUrl)
result.onSuccess { message ->
_state.value = _state.value.copy(
error = null,
saveMessage = message
)
}.onFailure { exception ->
_state.value = _state.value.copy(
error = exception.message ?: "保存图片失败"
)
}
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: "保存图片失败"
)
}
}
}
/**
* 清除保存消息
*/
fun clearSaveMessage() {
_state.value = _state.value.copy(saveMessage = null)
}
}