diff --git a/components/AttitudePanel/AttitudePanel.vue b/components/AttitudePanel/AttitudePanel.vue index 5e91f5c..22137d5 100644 --- a/components/AttitudePanel/AttitudePanel.vue +++ b/components/AttitudePanel/AttitudePanel.vue @@ -20,10 +20,13 @@ class="comment-input" type="text" v-model="commentText" + :disabled="sending" placeholder="说点什么吧..." placeholder-class="input-placeholder" /> - + @@ -35,6 +38,10 @@ const props = defineProps({ initialAction: { type: String, default: '' + }, + sending: { + type: Boolean, + default: false } }); @@ -62,19 +69,28 @@ const handleAction = (action) => { }; const handleSend = () => { - if (!commentText.value.trim()) { + const content = commentText.value.trim(); + if (!content) { uni.showToast({ title: '请输入评论内容', icon: 'none' }); return; } + if (props.sending) return; emit('send-comment', { action: currentAction.value, - content: commentText.value + content }); - commentText.value = ''; // 发送后清空 }; + +const resetComment = () => { + commentText.value = ''; +}; + +defineExpose({ + resetComment +}); \ No newline at end of file + diff --git a/components/CommentItem/CommentItem.vue b/components/CommentItem/CommentItem.vue index 46d6721..0c586f6 100644 --- a/components/CommentItem/CommentItem.vue +++ b/components/CommentItem/CommentItem.vue @@ -37,7 +37,7 @@ const props = defineProps({ } }); -const emit = defineEmits(['need-login']); +const emit = defineEmits(['need-login', 'like-change']); const userStore = useUserStore(); const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName); @@ -70,6 +70,11 @@ const handleLike = async () => { } else { currentLikes.value -= 1; } + emit('like-change', { + id: props.comment.id, + isLiked: isLiked.value, + likes: currentLikes.value + }); try { // 调用点赞接口 (点赞和取消点赞为同一个接口) @@ -85,7 +90,6 @@ const handleLike = async () => { page: 'rating_detail' } }); - uni.showToast({ title: '操作成功', icon: 'success' }); } catch (error) { // 接口调用失败,回滚 UI 状态 isLiked.value = !isLiked.value; @@ -94,6 +98,11 @@ const handleLike = async () => { } else { currentLikes.value -= 1; } + emit('like-change', { + id: props.comment.id, + isLiked: isLiked.value, + likes: currentLikes.value + }); uni.showToast({ title: '操作失败', icon: 'none' }); } finally { isLiking = false; diff --git a/pages/rating/detail.vue b/pages/rating/detail.vue index 823d18a..880b377 100644 --- a/pages/rating/detail.vue +++ b/pages/rating/detail.vue @@ -47,7 +47,9 @@ @@ -89,18 +91,32 @@ - + + + 评论加载中... + 正在拉取大家的锐评 + + + 评论加载失败 + {{ commentError }} + + + + 还没有评论 + 来发第一条锐评,带动一下讨论气氛 + - - {{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多了') }} + + {{ commentLoadText }} @@ -127,14 +143,22 @@ import { getShareReward } from "@/api/system"; import { useUserStore } from "@/stores/user"; const userStore = useUserStore(); const statusBarHeight = ref(getStatusBarHeight() || 44); -const loading = ref(false); const hasMore = ref(true); const currentPage = ref(1); const currentAction = ref(''); const successPopupRef = ref(null); const loginPopupRef = ref(null); +const attitudePanelRef = ref(null); const currentTab = ref('hot'); // 'hot' 或 'new' const isLoggedIn = computed(() => !!userStore.token || !!userStore.userInfo?.nickName); +const commentLoading = ref(false); +const isSendingComment = ref(false); +const commentError = ref(''); +const commentLoadText = computed(() => { + if (commentLoading.value && comments.value.length) return '加载中...'; + if (!hasMore.value) return '没有更多了'; + return '上拉加载更多'; +}); const goBack = () => { uni.navigateBack(); @@ -215,9 +239,10 @@ const handleSendComment = async (payload) => { return; } - if (!detailData.value.topicId || !itemId.value) return; + if (!detailData.value.topicId || !itemId.value || isSendingComment.value) return; try { + isSendingComment.value = true; uni.showLoading({ title: '发送中...' }); await topicItemComment({ topicId: detailData.value.topicId, @@ -237,7 +262,6 @@ const handleSendComment = async (payload) => { // 刷新页面数据以获取最新评论 currentPage.value = 1; hasMore.value = true; - comments.value = []; uni.$trackRecord({ eventName: 'comment', eventType: 'comment', @@ -248,13 +272,16 @@ const handleSendComment = async (payload) => { page: 'rating_detail' } }); - getCommentsData(); + attitudePanelRef.value?.resetComment?.(); + await getCommentsData({ replace: true }); } catch (error) { uni.hideLoading(); uni.showToast({ title: '发送失败', icon: 'none' }); + } finally { + isSendingComment.value = false; } }; @@ -263,16 +290,34 @@ const switchTab = (tab) => { currentTab.value = tab; currentPage.value = 1; hasMore.value = true; - // comments.value = []; // 移除这行,避免数据清空导致高度塌陷 - getCommentsData(true); // 传入标识表示是切换榜单 + commentError.value = ''; + getCommentsData({ replace: true }); }; -const getCommentsData = async (isSwitchTab = false) => { - if (!detailData.value.topicId || !itemId.value || loading.value) return; +const resolveCommentHasMore = (res, list) => { + const data = res?.data || res || {}; + const hasNext = data?.hasNext ?? res?.hasNext; + if (typeof hasNext === 'boolean') return hasNext; + + const pages = Number(data?.pages ?? res?.pages ?? 0); + if (pages > 0) return currentPage.value < pages; + + const total = Number(data?.total ?? res?.total ?? 0); + if (total > 0) { + const size = Number(data?.size ?? data?.pageSize ?? res?.size ?? res?.pageSize ?? list.length || 1); + return currentPage.value * size < total; + } + + return list.length > 0; +}; + +const getCommentsData = async ({ replace = false } = {}) => { + if (!detailData.value.topicId || !itemId.value || commentLoading.value) return; try { - loading.value = true; - const orderBy = currentTab.value; + commentLoading.value = true; + commentError.value = ''; + const orderBy = currentTab.value === 'hot' ? 1 : 2; const res = await topicItemCommentList(detailData.value.topicId, itemId.value, currentPage.value, orderBy); const rawList = res?.data?.records || @@ -297,21 +342,42 @@ const getCommentsData = async (isSwitchTab = false) => { userAction: item.userAction || "" // 夯/顶级/人上人/NPC/拉 })); - if (currentPage.value === 1 || isSwitchTab) { + if (currentPage.value === 1 || replace) { comments.value = formattedList; } else { comments.value = [...comments.value, ...formattedList]; } - // 假设每页10条,如果返回的少于10条则没有更多了 - hasMore.value = list.length >= 6; + hasMore.value = resolveCommentHasMore(res, list); } catch (error) { console.error('获取评论列表失败:', error); + commentError.value = '请检查网络后重试'; + if (currentPage.value > 1) { + currentPage.value -= 1; + } } finally { - loading.value = false; + commentLoading.value = false; } }; +const retryLoadComments = () => { + if (!comments.value.length) { + currentPage.value = 1; + } + getCommentsData({ replace: currentPage.value === 1 }); +}; + +const handleCommentLikeChange = ({ id, isLiked, likes }) => { + comments.value = comments.value.map((item) => { + if (item.id !== id) return item; + return { + ...item, + isLiked, + likes + }; + }); +}; + const getDetailData = async () => { if (!itemId.value) return; try { @@ -344,7 +410,7 @@ const getDetailData = async () => { // 拉取真实的评论列表 if (currentPage.value === 1) { - getCommentsData(); + getCommentsData({ replace: true }); } } catch (error) { console.error('获取详情失败:', error); @@ -377,6 +443,7 @@ const handleLoginSuccess = () => { currentPage.value = 1; hasMore.value = true; comments.value = []; + commentError.value = ''; getDetailData(); }; @@ -405,11 +472,12 @@ onPullDownRefresh(() => { currentPage.value = 1; hasMore.value = true; comments.value = []; + commentError.value = ''; getDetailData(); }); onReachBottom(() => { - if (!hasMore.value || loading.value) return; + if (!hasMore.value || commentLoading.value) return; currentPage.value += 1; getCommentsData(); }); @@ -749,6 +817,47 @@ onReachBottom(() => { .comment-list { display: flex; flex-direction: column; + min-height: 400rpx; +} + +.comment-state { + min-height: 400rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + color: #999; + padding: 40rpx 32rpx; + box-sizing: border-box; +} + +.comment-state-title { + font-size: 30rpx; + color: #222; + font-weight: 700; + margin-bottom: 12rpx; +} + +.comment-state-desc { + font-size: 24rpx; + color: #9aa0ae; + line-height: 1.6; +} + +.comment-state-btn { + margin-top: 24rpx; + height: 72rpx; + line-height: 72rpx; + padding: 0 32rpx; + border-radius: 999rpx; + background: #4d44f1; + color: #fff; + font-size: 26rpx; +} + +.comment-state-btn::after { + border: none; } .load-more {