1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-07-27 04:20:43 +08:00
Files
uni-halo/src/components/post-detail/uh-post-comment-panel.vue
T
小莫唐尼 9c028bdc1b style(comment-panel): 调整加载组件样式并添加加载文案
优化评论列表加载状态的视觉表现,增加黑色加载图标与加载中文案提示
2026-06-15 22:09:59 +08:00

407 lines
14 KiB
Vue
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.
<script setup lang="ts">
import { createComment, createReply, listCommentReplies, listComments } from '@/api/halo-base/comment'
import { completeUrl } from '@/utils/url'
import { avatar } from '@/utils/imageHelper'
import { timeFrom } from '@/utils/base/timeFrom'
import type { CommentV1alpha1PublicApiListCommentRepliesRequest, CommentV1alpha1PublicApiListComments1Request, CommentWithReplyVo, ReplyVo } from '@halo-dev/api-client'
import type { IHaloListResponseBase } from '@/api/types/halo'
interface IProps {
postName: string
commentSubjectVersion: string
commentCount?: number
}
const props = withDefaults(defineProps<IProps>(), {
commentCount: 0,
})
const emits = defineEmits<{
(e: 'close'): void
(e: 'commented'): void
}>()
// --- 评论列表 ---
const commentList = ref<CommentWithReplyVo[]>([])
const commentPage = ref(1)
const commentHasNext = ref(false)
const commentLoading = ref(false)
// --- 回复目标 ---
const replyTarget = ref<{
commentName: string
replyName?: string
displayName: string
} | null>(null)
// --- 输入内容 ---
const inputText = ref('')
const submitLoading = ref(false)
// --- 展开回复的评论 ---
const expandedReplies = ref<Set<string>>(new Set())
const replyLoadingMap = ref<Record<string, boolean>>({})
const replyHasNextMap = ref<Record<string, boolean>>({})
const replyPageMap = ref<Record<string, number>>({})
// --- 获取评论列表 ---
async function fetchComments(page = 1, append = false) {
commentLoading.value = true
try {
const res = await listComments({
group: 'content.halo.run',
name: props.postName,
kind: 'Post',
version: props.commentSubjectVersion,
page,
size: 20,
sort: ['metadata.creationTimestamp,desc'],
} as CommentV1alpha1PublicApiListComments1Request)
const data = res as unknown as IHaloListResponseBase<CommentWithReplyVo>
if (append) {
commentList.value = [...commentList.value, ...(data.items ?? [])]
}
else {
commentList.value = data.items ?? []
}
commentHasNext.value = data.hasNext
commentPage.value = page
}
catch (err) {
console.error('获取评论失败:', err)
}
finally {
commentLoading.value = false
}
}
// --- 加载更多评论 ---
function handleLoadMoreComments() {
if (commentLoading.value || !commentHasNext.value)
return
fetchComments(commentPage.value + 1, true)
}
// --- 获取回复列表 ---
async function fetchReplies(commentName: string, page = 1, append = false) {
replyLoadingMap.value[commentName] = true
try {
const res = await listCommentReplies({
name: commentName,
page,
size: 10,
} as CommentV1alpha1PublicApiListCommentRepliesRequest)
const data = res as unknown as IHaloListResponseBase<ReplyVo>
const comment = commentList.value.find(c => c.metadata.name === commentName)
if (comment) {
if (append) {
comment.replies = {
...comment.replies,
items: [...(comment.replies?.items ?? []), ...(data.items ?? [])],
hasNext: data.hasNext,
} as any
}
else {
comment.replies = {
...data,
items: data.items ?? [],
} as any
}
}
replyHasNextMap.value[commentName] = data.hasNext
replyPageMap.value[commentName] = page
}
catch (err) {
console.error('获取回复失败:', err)
uni.showToast({ title: '获取回复失败', icon: 'none' })
}
finally {
replyLoadingMap.value[commentName] = false
}
}
// --- 切换展开回复 ---
function handleToggleReplies(comment: CommentWithReplyVo) {
const name = comment.metadata.name
if (expandedReplies.value.has(name)) {
expandedReplies.value.delete(name)
}
else {
expandedReplies.value.add(name)
if (!comment.replies?.items?.length) {
fetchReplies(name)
}
}
}
// --- 加载更多回复 ---
function handleLoadMoreReplies(commentName: string) {
if (replyLoadingMap.value[commentName] || !replyHasNextMap.value[commentName])
return
fetchReplies(commentName, (replyPageMap.value[commentName] ?? 1) + 1, true)
}
// --- 设置回复目标 ---
function handleReply(commentName: string, replyName: string | undefined, displayName: string) {
replyTarget.value = { commentName, replyName, displayName }
}
// --- 取消回复 ---
function handleCancelReply() {
replyTarget.value = null
}
// --- 提交评论/回复 ---
async function handleSubmit() {
const text = inputText.value.trim()
if (!text || submitLoading.value)
return
submitLoading.value = true
try {
if (replyTarget.value) {
await createReply({
name: replyTarget.value.commentName,
replyRequest: {
content: text,
raw: text,
quoteReply: replyTarget.value.replyName || undefined,
allowNotification: true,
},
} as any)
}
else {
await createComment({
commentRequest: {
content: text,
raw: text,
subjectRef: {
group: 'content.halo.run',
kind: 'Post',
name: props.postName,
version: props.commentSubjectVersion,
},
allowNotification: true,
},
} as any)
}
inputText.value = ''
replyTarget.value = null
uni.showToast({ title: '评论成功', icon: 'none' })
await fetchComments(1)
emits('commented')
}
catch (err) {
console.error('评论失败:', err)
uni.showToast({ title: '评论失败', icon: 'none' })
}
finally {
submitLoading.value = false
}
}
// --- 关闭面板 ---
function handleClose() {
emits('close')
}
defineExpose({
refresh: () => fetchComments(),
})
onMounted(() => {
fetchComments()
})
</script>
<template>
<view class="uh-backdrop-blur fixed inset-0 z-110 flex items-end bg-black/20" @click="handleClose">
<wd-transition :show="true" :duration="300" name="slide-up" :lazy-render="false">
<!-- 面板主体 -->
<view
class="box-border w-screen flex flex-col rounded-t-3xl bg-white"
@click.stop
>
<!-- 拖拽指示条 -->
<view class="w-full center py-2" @click.stop="handleClose">
<view class="h-1 w-10 rounded-full bg-gray-300" />
</view>
<!-- 头部 -->
<view class="box-border flex items-center justify-between border-b border-gray-100 px-4 pb-3">
<text class="text-base text-gray-900 font-bold">
说点什么 <text v-if="commentCount > 0" class="text-sm text-gray-600 font-normal">{{ commentCount }}</text>
</text>
<view class="h-8 w-8 center rounded-full bg-gray-50" @click="handleClose">
<wd-icon name="close" color="#6B7280" :size="18" />
</view>
</view>
<!-- 评论列表 -->
<scroll-view
class="h-[60vh] overflow-hidden"
scroll-y
@scrolltolower="handleLoadMoreComments"
>
<view class="box-border h-full px-4 pt-3">
<!-- 加载中 -->
<view v-if="commentLoading && commentList.length === 0" class="box-border h-full center">
<wd-loading />
</view>
<!-- 空状态 -->
<view v-else-if="!commentLoading && commentList.length === 0" class="box-border h-full center flex-col gap-y-2">
<wd-icon name="chat" color="#D1D5DB" :size="40" />
<text class="text-sm text-gray-400">快来抢沙发吧~</text>
</view>
<!-- 评论列表 -->
<view v-else class="box-border h-full w-full flex flex-col">
<view
v-for="comment in commentList"
:key="comment.metadata.name"
class="border-b border-gray-50 py-3"
>
<view class="flex gap-x-3">
<!-- 头像 -->
<image
class="uh-shadow-md box-border h-9 w-9 shrink-0 border-2 border-white rounded-xl border-solid bg-gray-100"
:src="avatar(comment.owner.avatar)"
mode="aspectFill"
/>
<!-- 内容区 -->
<view class="flex flex-1 flex-col gap-y-2 overflow-hidden">
<view class="flex items-center justify-between">
<text class="text-xs text-gray-500 font-medium">{{ comment.owner.displayName || '匿名用户' }}</text>
<text class="text-[11px] text-gray-400">{{ timeFrom(comment.spec.creationTime || comment.metadata.creationTimestamp) }}</text>
</view>
<text class="text-sm text-gray-900 leading-5">
<rich-text :nodes="comment.spec.raw" />
</text>
<!-- 操作栏 -->
<view class="flex items-center gap-x-4">
<view
class="flex items-center gap-x-1"
@click="handleReply(comment.metadata.name, undefined, comment.owner.displayName || '匿名用户')"
>
<wd-icon name="message" color="#9CA3AF" :size="14" />
<text class="text-[11px] text-gray-400">回复</text>
</view>
<view
v-if="(comment.status?.replyCount ?? 0) > 0"
class="flex items-center gap-x-1"
@click="handleToggleReplies(comment)"
>
<wd-icon
:name="expandedReplies.has(comment.metadata.name) ? 'up' : 'down'"
color="#9CA3AF"
:size="14"
/>
<text class="text-[11px] text-gray-400">
{{ expandedReplies.has(comment.metadata.name) ? '收起' : `${comment.status?.replyCount}条回复` }}
</text>
</view>
</view>
<!-- 回复列表 -->
<view v-if="expandedReplies.has(comment.metadata.name)" class="mt-2 flex flex-col gap-y-2 rounded-xl bg-gray-50 p-3">
<view
v-for="reply in (comment.replies?.items ?? [])"
:key="reply.metadata.name"
class="flex gap-x-2.5"
>
<image
class="h-7 w-7 shrink-0 border-2 border-white rounded-lg border-solid bg-gray-200"
:src="avatar(reply.owner.avatar)"
mode="aspectFill"
/>
<view class="flex flex-1 flex-col gap-y-1.5 overflow-hidden">
<view class="flex items-center justify-between">
<text class="text-[11px] text-gray-500 font-medium">{{ reply.owner.displayName || '匿名用户' }}</text>
<text class="text-[10px] text-gray-400">{{ timeFrom(reply.spec.creationTime || reply.metadata.creationTimestamp) }}</text>
</view>
<view v-if="reply.spec.quoteReply" class="rounded-lg bg-gray-100 px-2.5 py-1.5">
<text class="text-[11px] text-gray-400">回复 {{ reply.spec.quoteReply }}</text>
</view>
<text class="text-xs text-gray-700 leading-4.5">
<rich-text :nodes="reply.spec.raw" />
</text>
<view
class="flex items-center gap-x-1"
@click="handleReply(comment.metadata.name, reply.metadata.name, reply.owner.displayName || '匿名用户')"
>
<wd-icon name="message" color="#9CA3AF" :size="12" />
<text class="text-[10px] text-gray-400">回复</text>
</view>
</view>
</view>
<!-- 加载更多回复 -->
<view
v-if="replyHasNextMap[comment.metadata.name]"
class="center py-1"
@click="handleLoadMoreReplies(comment.metadata.name)"
>
<text class="text-xs text-gray-400">
{{ replyLoadingMap[comment.metadata.name] ? '加载中...' : '查看更多回复' }}
</text>
</view>
<view v-if="replyLoadingMap[comment.metadata.name] && !(comment.replies?.items?.length)" class="center py-2">
<wd-loading :size="20" color="#1a1a1a" > 加载中 </wd-loading>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="commentHasNext" class="center py-3" @click="handleLoadMoreComments">
<text class="text-xs text-gray-400">{{ commentLoading ? '加载中...' : '加载更多评论' }}</text>
</view>
<view class="h-4 w-full" />
</view>
</view>
</scroll-view>
<!-- 底部输入区 -->
<view class="box-border border-t border-gray-100 px-4 pt-2">
<!-- 回复提示 -->
<view v-if="replyTarget" class="mb-2 flex items-center justify-between rounded-lg bg-gray-50 px-3 py-1.5">
<text class="text-xs text-gray-500">回复 {{ replyTarget.displayName }}</text>
<view @click="handleCancelReply">
<wd-icon name="close" color="#9CA3AF" :size="14" />
</view>
</view>
<view class="flex items-center gap-x-3 pb-2">
<input
v-model="inputText"
class="flex-1 rounded-xl bg-gray-50 px-4 py-2.5 text-sm active:bg-gray-100 focus:bg-gray-100"
:placeholder="replyTarget ? `回复 ${replyTarget.displayName}...` : '写评论...'"
confirm-type="send"
:adjust-position="true"
@confirm="handleSubmit"
>
<view
class="h-9 w-9 center shrink-0 rounded-xl transition-colors"
:class="inputText.trim() ? 'bg-gray-900' : 'bg-gray-200'"
@click="handleSubmit"
>
<wd-icon name="arrow-up" :color="inputText.trim() ? '#FFFFFF' : '#9CA3AF'" :size="18" />
</view>
</view>
</view>
</view>
</wd-transition>
</view>
</template>