diff --git a/src/modules/contents/comments/CommentList.vue b/src/modules/contents/comments/CommentList.vue index b162c25a73..53ce792bf1 100644 --- a/src/modules/contents/comments/CommentList.vue +++ b/src/modules/contents/comments/CommentList.vue @@ -15,187 +15,18 @@ import { } from "@halo-dev/components"; import CommentListItem from "./components/CommentListItem.vue"; import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue"; -import type { - ListedComment, - ListedCommentList, - User, -} from "@halo-dev/api-client"; -import { computed, onMounted, ref, watch } from "vue"; +import type { ListedComment, User } from "@halo-dev/api-client"; +import { computed, ref, watch } from "vue"; import { apiClient } from "@/utils/api-client"; -import { onBeforeRouteLeave } from "vue-router"; import FilterTag from "@/components/filter/FilterTag.vue"; import FilterCleanButton from "@/components/filter/FilterCleanButton.vue"; import { getNode } from "@formkit/core"; +import { useQuery } from "@tanstack/vue-query"; -const comments = ref({ - page: 1, - size: 20, - total: 0, - items: [], - first: true, - last: false, - hasNext: false, - hasPrevious: false, - totalPages: 0, -}); -const loading = ref(false); const checkAll = ref(false); const selectedComment = ref(); const selectedCommentNames = ref([]); const keyword = ref(""); -const refreshInterval = ref(); - -const handleFetchComments = async (options?: { - mute?: boolean; - page?: number; -}) => { - try { - clearInterval(refreshInterval.value); - - if (!options?.mute) { - loading.value = true; - } - - if (options?.page) { - comments.value.page = options.page; - } - - const { data } = await apiClient.comment.listComments({ - page: comments.value.page, - size: comments.value.size, - approved: selectedApprovedFilterItem.value.value, - sort: selectedSortFilterItem.value.value, - keyword: keyword.value, - ownerName: selectedUser.value?.metadata.name, - }); - comments.value = data; - - const deletedComments = comments.value.items.filter( - (comment) => !!comment.comment.metadata.deletionTimestamp - ); - - if (deletedComments.length) { - refreshInterval.value = setInterval(() => { - handleFetchComments({ mute: true }); - }, 3000); - } - } catch (error) { - console.error("Failed to fetch comments", error); - } finally { - loading.value = false; - } -}; - -onBeforeRouteLeave(() => { - clearInterval(refreshInterval.value); -}); - -const handlePaginationChange = ({ - page, - size, -}: { - page: number; - size: number; -}) => { - comments.value.page = page; - comments.value.size = size; - handleFetchComments(); -}; - -// Selection -const handleCheckAllChange = (e: Event) => { - const { checked } = e.target as HTMLInputElement; - - if (checked) { - selectedCommentNames.value = - comments.value.items.map((comment) => { - return comment.comment.metadata.name; - }) || []; - } else { - selectedCommentNames.value = []; - } -}; - -const checkSelection = (comment: ListedComment) => { - return ( - comment.comment.metadata.name === - selectedComment.value?.comment.metadata.name || - selectedCommentNames.value.includes(comment.comment.metadata.name) - ); -}; - -watch( - () => selectedCommentNames.value, - (newValue) => { - checkAll.value = newValue.length === comments.value.items?.length; - } -); - -const handleDeleteInBatch = async () => { - Dialog.warning({ - title: "确定要删除所选的评论吗?", - description: "将同时删除所有评论下的回复,该操作不可恢复。", - confirmType: "danger", - onConfirm: async () => { - try { - const promises = selectedCommentNames.value.map((name) => { - return apiClient.extension.comment.deletecontentHaloRunV1alpha1Comment( - { - name, - } - ); - }); - await Promise.all(promises); - selectedCommentNames.value = []; - - Toast.success("删除成功"); - } catch (e) { - console.error("Failed to delete comments", e); - } finally { - await handleFetchComments(); - } - }, - }); -}; - -const handleApproveInBatch = async () => { - Dialog.warning({ - title: "确定要审核通过所选的评论吗?", - onConfirm: async () => { - try { - const commentsToUpdate = comments.value.items.filter((comment) => { - return ( - selectedCommentNames.value.includes( - comment.comment.metadata.name - ) && !comment.comment.spec.approved - ); - }); - const promises = commentsToUpdate.map((comment) => { - const commentToUpdate = comment.comment; - commentToUpdate.spec.approved = true; - // TODO: 暂时由前端设置发布时间。see https://github.com/halo-dev/halo/pull/2746 - commentToUpdate.spec.approvedTime = new Date().toISOString(); - return apiClient.extension.comment.updatecontentHaloRunV1alpha1Comment( - { - name: commentToUpdate.metadata.name, - comment: commentToUpdate, - } - ); - }); - await Promise.all(promises); - selectedCommentNames.value = []; - - Toast.success("操作成功"); - } catch (e) { - console.error("Failed to approve comments in batch", e); - } finally { - await handleFetchComments(); - } - }, - }); -}; - -onMounted(handleFetchComments); // Filters const ApprovedFilterItems: { label: string; value?: boolean }[] = [ @@ -254,7 +85,7 @@ const handleApprovedFilterItemChange = (filterItem: { }) => { selectedApprovedFilterItem.value = filterItem; selectedCommentNames.value = []; - handleFetchComments({ page: 1 }); + page.value = 1; }; const handleSortFilterItemChange = (filterItem: { @@ -263,12 +94,12 @@ const handleSortFilterItemChange = (filterItem: { }) => { selectedSortFilterItem.value = filterItem; selectedCommentNames.value = []; - handleFetchComments({ page: 1 }); + page.value = 1; }; function handleSelectUser(user: User | undefined) { selectedUser.value = user; - handleFetchComments({ page: 1 }); + page.value = 1; } function handleKeywordChange() { @@ -276,12 +107,12 @@ function handleKeywordChange() { if (keywordNode) { keyword.value = keywordNode._value as string; } - handleFetchComments({ page: 1 }); + page.value = 1; } function handleClearKeyword() { keyword.value = ""; - handleFetchComments({ page: 1 }); + page.value = 1; } const hasFilters = computed(() => { @@ -298,8 +129,148 @@ function handleClearFilters() { selectedSortFilterItem.value = SortFilterItems[0]; selectedUser.value = undefined; keyword.value = ""; - handleFetchComments({ page: 1 }); + page.value = 1; } + +const page = ref(1); +const size = ref(20); +const total = ref(0); + +const { + data: comments, + isLoading, + isFetching, + refetch, +} = useQuery({ + queryKey: [ + "comments", + page, + size, + selectedApprovedFilterItem, + selectedSortFilterItem, + selectedUser, + keyword, + ], + queryFn: async () => { + const { data } = await apiClient.comment.listComments({ + page: page.value, + size: size.value, + approved: selectedApprovedFilterItem.value.value, + sort: selectedSortFilterItem.value.value, + keyword: keyword.value, + ownerName: selectedUser.value?.metadata.name, + }); + + total.value = data.total; + + return data.items; + }, + refetchOnWindowFocus: false, + refetchInterval(data) { + const deletingComments = data?.filter( + (comment) => !!comment.comment.metadata.deletionTimestamp + ); + return deletingComments?.length ? 3000 : false; + }, +}); + +// Selection +const handleCheckAllChange = (e: Event) => { + const { checked } = e.target as HTMLInputElement; + + if (checked) { + selectedCommentNames.value = + comments.value?.map((comment) => { + return comment.comment.metadata.name; + }) || []; + } else { + selectedCommentNames.value = []; + } +}; + +const checkSelection = (comment: ListedComment) => { + return ( + comment.comment.metadata.name === + selectedComment.value?.comment.metadata.name || + selectedCommentNames.value.includes(comment.comment.metadata.name) + ); +}; + +watch( + () => selectedCommentNames.value, + (newValue) => { + checkAll.value = newValue.length === comments.value?.length; + } +); + +const handleDeleteInBatch = async () => { + Dialog.warning({ + title: "确定要删除所选的评论吗?", + description: "将同时删除所有评论下的回复,该操作不可恢复。", + confirmType: "danger", + onConfirm: async () => { + try { + const promises = selectedCommentNames.value.map((name) => { + return apiClient.extension.comment.deletecontentHaloRunV1alpha1Comment( + { + name, + } + ); + }); + await Promise.all(promises); + selectedCommentNames.value = []; + + Toast.success("删除成功"); + } catch (e) { + console.error("Failed to delete comments", e); + } finally { + refetch(); + } + }, + }); +}; + +const handleApproveInBatch = async () => { + Dialog.warning({ + title: "确定要审核通过所选的评论吗?", + onConfirm: async () => { + try { + const commentsToUpdate = comments.value?.filter((comment) => { + return ( + selectedCommentNames.value.includes( + comment.comment.metadata.name + ) && !comment.comment.spec.approved + ); + }); + + const promises = commentsToUpdate?.map((comment) => { + return apiClient.extension.comment.updatecontentHaloRunV1alpha1Comment( + { + name: comment.comment.metadata.name, + comment: { + ...comment.comment, + spec: { + ...comment.comment.spec, + approved: true, + // TODO: 暂时由前端设置发布时间。see https://github.com/halo-dev/halo/pull/2746 + approvedTime: new Date().toISOString(), + }, + }, + } + ); + }); + await Promise.all(promises || []); + selectedCommentNames.value = []; + + Toast.success("操作成功"); + } catch (e) { + console.error("Failed to approve comments in batch", e); + } finally { + refetch(); + } + }, + }); +}; - - + + @@ -492,14 +463,11 @@ function handleClearFilters() { class="box-border h-full w-full divide-y divide-gray-100" role="list" > -
  • +
  • diff --git a/src/modules/contents/comments/components/CommentListItem.vue b/src/modules/contents/comments/components/CommentListItem.vue index e415975861..67ab5d7a49 100644 --- a/src/modules/contents/comments/components/CommentListItem.vue +++ b/src/modules/contents/comments/components/CommentListItem.vue @@ -22,12 +22,13 @@ import type { SinglePage, } from "@halo-dev/api-client"; import { formatDatetime } from "@/utils/date"; -import { computed, onMounted, provide, ref, watch, type Ref } from "vue"; +import { computed, provide, ref, watch, type Ref } from "vue"; import ReplyListItem from "./ReplyListItem.vue"; import { apiClient } from "@/utils/api-client"; -import { onBeforeRouteLeave, type RouteLocationRaw } from "vue-router"; +import type { RouteLocationRaw } from "vue-router"; import cloneDeep from "lodash.clonedeep"; import { usePermission } from "@/utils/permission"; +import { useQuery } from "@tanstack/vue-query"; const { currentUserHasPermission } = usePermission(); @@ -46,13 +47,10 @@ const emit = defineEmits<{ (event: "reload"): void; }>(); -const replies = ref([] as ListedReply[]); const selectedReply = ref(); const hoveredReply = ref(); -const loading = ref(false); const showReplies = ref(false); const replyModal = ref(false); -const refreshInterval = ref(); provide>("hoveredReply", hoveredReply); @@ -82,26 +80,30 @@ const handleApproveReplyInBatch = async () => { title: "确定要审核通过该评论的所有回复吗?", onConfirm: async () => { try { - const repliesToUpdate = replies.value.filter((reply) => { + const repliesToUpdate = replies.value?.filter((reply) => { return !reply.reply.spec.approved; }); - const promises = repliesToUpdate.map((reply) => { - const replyToUpdate = reply.reply; - replyToUpdate.spec.approved = true; - // TODO: 暂时由前端设置发布时间。see https://github.com/halo-dev/halo/pull/2746 - replyToUpdate.spec.approvedTime = new Date().toISOString(); + const promises = repliesToUpdate?.map((reply) => { return apiClient.extension.reply.updatecontentHaloRunV1alpha1Reply({ - name: replyToUpdate.metadata.name, - reply: replyToUpdate, + name: reply.reply.metadata.name, + reply: { + ...reply.reply, + spec: { + ...reply.reply.spec, + approved: true, + // TODO: 暂时由前端设置发布时间。see https://github.com/halo-dev/halo/pull/2746 + approvedTime: new Date().toISOString(), + }, + }, }); }); - await Promise.all(promises); + await Promise.all(promises || []); Toast.success("操作成功"); } catch (e) { console.error("Failed to approve comment replies in batch", e); } finally { - await handleFetchReplies(); + await refetch(); } }, }); @@ -126,66 +128,46 @@ const handleApprove = async () => { } }; -const handleFetchReplies = async (options?: { mute?: boolean }) => { - try { - clearInterval(refreshInterval.value); - - if (!options?.mute) { - loading.value = true; - } - +const { + data: replies, + isLoading, + refetch, +} = useQuery({ + queryKey: [ + "comment-replies", + props.comment.comment.metadata.name, + showReplies, + ], + queryFn: async () => { const { data } = await apiClient.reply.listReplies({ commentName: props.comment.comment.metadata.name, page: 0, size: 0, }); - replies.value = data.items; - - const deletedReplies = replies.value.filter( + return data.items; + }, + refetchOnWindowFocus: false, + refetchInterval(data) { + const deletingReplies = data?.filter( (reply) => !!reply.reply.metadata.deletionTimestamp ); - - if (deletedReplies.length) { - refreshInterval.value = setInterval(() => { - handleFetchReplies({ mute: true }); - }, 3000); - } - } catch (error) { - console.error("Failed to fetch comment replies", error); - } finally { - loading.value = false; - } -}; - -onMounted(() => { - clearInterval(refreshInterval.value); -}); - -onBeforeRouteLeave(() => { - clearInterval(refreshInterval.value); + return deletingReplies?.length ? 3000 : false; + }, + enabled: computed(() => showReplies.value), }); -watch( - () => showReplies.value, - (newValue) => { - if (newValue) { - handleFetchReplies(); - } else { - replies.value = []; - } - } -); - const handleToggleShowReplies = async () => { showReplies.value = !showReplies.value; if (showReplies.value) { // update last read time - const commentToUpdate = cloneDeep(props.comment.comment); - commentToUpdate.spec.lastReadTime = new Date().toISOString(); - await apiClient.extension.comment.updatecontentHaloRunV1alpha1Comment({ - name: commentToUpdate.metadata.name, - comment: commentToUpdate, - }); + if (props.comment.comment.status?.unreadReplyCount) { + const commentToUpdate = cloneDeep(props.comment.comment); + commentToUpdate.spec.lastReadTime = new Date().toISOString(); + await apiClient.extension.comment.updatecontentHaloRunV1alpha1Comment({ + name: commentToUpdate.metadata.name, + comment: commentToUpdate, + }); + } } else { emit("reload"); } @@ -202,7 +184,7 @@ const onTriggerReply = (reply: ListedReply) => { const onReplyCreationModalClose = () => { selectedReply.value = undefined; - handleFetchReplies({ mute: true }); + refetch(); }; // Subject ref processing @@ -413,12 +395,12 @@ const subjectRefResult = computed(() => {
    - - + +