mirror of https://github.com/halo-dev/halo
refactor: use tanstack query to refactor comment-related fetching (halo-dev/console#892)
#### What type of PR is this? /kind improvement #### What this PR does / why we need it: 使用 [TanStack Query](https://github.com/TanStack/query) 重构评论管理相关数据请求的相关逻辑。 #### Which issue(s) this PR fixes: Ref https://github.com/halo-dev/halo/issues/3360 #### Special notes for your reviewer: 测试方式: 1. 测试评论管理列表的数据请求 + 条件筛选无异常即可。 #### Does this PR introduce a user-facing change? ```release-note None ```pull/3445/head
parent
9646d411da
commit
bd9c4dabf7
|
@ -15,187 +15,18 @@ import {
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import CommentListItem from "./components/CommentListItem.vue";
|
import CommentListItem from "./components/CommentListItem.vue";
|
||||||
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
|
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
|
||||||
import type {
|
import type { ListedComment, User } from "@halo-dev/api-client";
|
||||||
ListedComment,
|
import { computed, ref, watch } from "vue";
|
||||||
ListedCommentList,
|
|
||||||
User,
|
|
||||||
} from "@halo-dev/api-client";
|
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
const comments = ref<ListedCommentList>({
|
|
||||||
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 checkAll = ref(false);
|
||||||
const selectedComment = ref<ListedComment>();
|
const selectedComment = ref<ListedComment>();
|
||||||
const selectedCommentNames = ref<string[]>([]);
|
const selectedCommentNames = ref<string[]>([]);
|
||||||
const keyword = 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
|
// Filters
|
||||||
const ApprovedFilterItems: { label: string; value?: boolean }[] = [
|
const ApprovedFilterItems: { label: string; value?: boolean }[] = [
|
||||||
|
@ -254,7 +85,7 @@ const handleApprovedFilterItemChange = (filterItem: {
|
||||||
}) => {
|
}) => {
|
||||||
selectedApprovedFilterItem.value = filterItem;
|
selectedApprovedFilterItem.value = filterItem;
|
||||||
selectedCommentNames.value = [];
|
selectedCommentNames.value = [];
|
||||||
handleFetchComments({ page: 1 });
|
page.value = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSortFilterItemChange = (filterItem: {
|
const handleSortFilterItemChange = (filterItem: {
|
||||||
|
@ -263,12 +94,12 @@ const handleSortFilterItemChange = (filterItem: {
|
||||||
}) => {
|
}) => {
|
||||||
selectedSortFilterItem.value = filterItem;
|
selectedSortFilterItem.value = filterItem;
|
||||||
selectedCommentNames.value = [];
|
selectedCommentNames.value = [];
|
||||||
handleFetchComments({ page: 1 });
|
page.value = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleSelectUser(user: User | undefined) {
|
function handleSelectUser(user: User | undefined) {
|
||||||
selectedUser.value = user;
|
selectedUser.value = user;
|
||||||
handleFetchComments({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeywordChange() {
|
function handleKeywordChange() {
|
||||||
|
@ -276,12 +107,12 @@ function handleKeywordChange() {
|
||||||
if (keywordNode) {
|
if (keywordNode) {
|
||||||
keyword.value = keywordNode._value as string;
|
keyword.value = keywordNode._value as string;
|
||||||
}
|
}
|
||||||
handleFetchComments({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClearKeyword() {
|
function handleClearKeyword() {
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
handleFetchComments({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFilters = computed(() => {
|
const hasFilters = computed(() => {
|
||||||
|
@ -298,8 +129,148 @@ function handleClearFilters() {
|
||||||
selectedSortFilterItem.value = SortFilterItems[0];
|
selectedSortFilterItem.value = SortFilterItems[0];
|
||||||
selectedUser.value = undefined;
|
selectedUser.value = undefined;
|
||||||
keyword.value = "";
|
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<ListedComment[]>({
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VPageHeader title="评论">
|
<VPageHeader title="评论">
|
||||||
|
@ -463,11 +434,11 @@ function handleClearFilters() {
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div
|
<div
|
||||||
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
||||||
@click="handleFetchComments()"
|
@click="refetch()"
|
||||||
>
|
>
|
||||||
<IconRefreshLine
|
<IconRefreshLine
|
||||||
v-tooltip="`刷新`"
|
v-tooltip="`刷新`"
|
||||||
:class="{ 'animate-spin text-gray-900': loading }"
|
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -477,12 +448,12 @@ function handleClearFilters() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!comments.items.length" appear name="fade">
|
<Transition v-else-if="!comments?.length" appear name="fade">
|
||||||
<VEmpty message="你可以尝试刷新或者修改筛选条件" title="当前没有评论">
|
<VEmpty message="你可以尝试刷新或者修改筛选条件" title="当前没有评论">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton @click="handleFetchComments">刷新</VButton>
|
<VButton @click="refetch">刷新</VButton>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
</template>
|
</template>
|
||||||
</VEmpty>
|
</VEmpty>
|
||||||
|
@ -492,14 +463,11 @@ function handleClearFilters() {
|
||||||
class="box-border h-full w-full divide-y divide-gray-100"
|
class="box-border h-full w-full divide-y divide-gray-100"
|
||||||
role="list"
|
role="list"
|
||||||
>
|
>
|
||||||
<li
|
<li v-for="comment in comments" :key="comment.comment.metadata.name">
|
||||||
v-for="comment in comments.items"
|
|
||||||
:key="comment.comment.metadata.name"
|
|
||||||
>
|
|
||||||
<CommentListItem
|
<CommentListItem
|
||||||
:comment="comment"
|
:comment="comment"
|
||||||
:is-selected="checkSelection(comment)"
|
:is-selected="checkSelection(comment)"
|
||||||
@reload="handleFetchComments({ mute: true })"
|
@reload="refetch()"
|
||||||
>
|
>
|
||||||
<template #checkbox>
|
<template #checkbox>
|
||||||
<input
|
<input
|
||||||
|
@ -518,11 +486,10 @@ function handleClearFilters() {
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="bg-white sm:flex sm:items-center sm:justify-end">
|
<div class="bg-white sm:flex sm:items-center sm:justify-end">
|
||||||
<VPagination
|
<VPagination
|
||||||
:page="comments.page"
|
v-model:page="page"
|
||||||
:size="comments.size"
|
v-model:size="size"
|
||||||
:total="comments.total"
|
:total="total"
|
||||||
:size-options="[20, 30, 50, 100]"
|
:size-options="[20, 30, 50, 100]"
|
||||||
@change="handlePaginationChange"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -22,12 +22,13 @@ import type {
|
||||||
SinglePage,
|
SinglePage,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
import { formatDatetime } from "@/utils/date";
|
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 ReplyListItem from "./ReplyListItem.vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
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 cloneDeep from "lodash.clonedeep";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
@ -46,13 +47,10 @@ const emit = defineEmits<{
|
||||||
(event: "reload"): void;
|
(event: "reload"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const replies = ref<ListedReply[]>([] as ListedReply[]);
|
|
||||||
const selectedReply = ref<ListedReply>();
|
const selectedReply = ref<ListedReply>();
|
||||||
const hoveredReply = ref<ListedReply>();
|
const hoveredReply = ref<ListedReply>();
|
||||||
const loading = ref(false);
|
|
||||||
const showReplies = ref(false);
|
const showReplies = ref(false);
|
||||||
const replyModal = ref(false);
|
const replyModal = ref(false);
|
||||||
const refreshInterval = ref();
|
|
||||||
|
|
||||||
provide<Ref<ListedReply | undefined>>("hoveredReply", hoveredReply);
|
provide<Ref<ListedReply | undefined>>("hoveredReply", hoveredReply);
|
||||||
|
|
||||||
|
@ -82,26 +80,30 @@ const handleApproveReplyInBatch = async () => {
|
||||||
title: "确定要审核通过该评论的所有回复吗?",
|
title: "确定要审核通过该评论的所有回复吗?",
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
const repliesToUpdate = replies.value.filter((reply) => {
|
const repliesToUpdate = replies.value?.filter((reply) => {
|
||||||
return !reply.reply.spec.approved;
|
return !reply.reply.spec.approved;
|
||||||
});
|
});
|
||||||
const promises = repliesToUpdate.map((reply) => {
|
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();
|
|
||||||
return apiClient.extension.reply.updatecontentHaloRunV1alpha1Reply({
|
return apiClient.extension.reply.updatecontentHaloRunV1alpha1Reply({
|
||||||
name: replyToUpdate.metadata.name,
|
name: reply.reply.metadata.name,
|
||||||
reply: replyToUpdate,
|
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("操作成功");
|
Toast.success("操作成功");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to approve comment replies in batch", e);
|
console.error("Failed to approve comment replies in batch", e);
|
||||||
} finally {
|
} finally {
|
||||||
await handleFetchReplies();
|
await refetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -126,66 +128,46 @@ const handleApprove = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetchReplies = async (options?: { mute?: boolean }) => {
|
const {
|
||||||
try {
|
data: replies,
|
||||||
clearInterval(refreshInterval.value);
|
isLoading,
|
||||||
|
refetch,
|
||||||
if (!options?.mute) {
|
} = useQuery<ListedReply[]>({
|
||||||
loading.value = true;
|
queryKey: [
|
||||||
}
|
"comment-replies",
|
||||||
|
props.comment.comment.metadata.name,
|
||||||
|
showReplies,
|
||||||
|
],
|
||||||
|
queryFn: async () => {
|
||||||
const { data } = await apiClient.reply.listReplies({
|
const { data } = await apiClient.reply.listReplies({
|
||||||
commentName: props.comment.comment.metadata.name,
|
commentName: props.comment.comment.metadata.name,
|
||||||
page: 0,
|
page: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
});
|
});
|
||||||
replies.value = data.items;
|
return data.items;
|
||||||
|
},
|
||||||
const deletedReplies = replies.value.filter(
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval(data) {
|
||||||
|
const deletingReplies = data?.filter(
|
||||||
(reply) => !!reply.reply.metadata.deletionTimestamp
|
(reply) => !!reply.reply.metadata.deletionTimestamp
|
||||||
);
|
);
|
||||||
|
return deletingReplies?.length ? 3000 : false;
|
||||||
if (deletedReplies.length) {
|
},
|
||||||
refreshInterval.value = setInterval(() => {
|
enabled: computed(() => showReplies.value),
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => showReplies.value,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
handleFetchReplies();
|
|
||||||
} else {
|
|
||||||
replies.value = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleShowReplies = async () => {
|
const handleToggleShowReplies = async () => {
|
||||||
showReplies.value = !showReplies.value;
|
showReplies.value = !showReplies.value;
|
||||||
if (showReplies.value) {
|
if (showReplies.value) {
|
||||||
// update last read time
|
// update last read time
|
||||||
const commentToUpdate = cloneDeep(props.comment.comment);
|
if (props.comment.comment.status?.unreadReplyCount) {
|
||||||
commentToUpdate.spec.lastReadTime = new Date().toISOString();
|
const commentToUpdate = cloneDeep(props.comment.comment);
|
||||||
await apiClient.extension.comment.updatecontentHaloRunV1alpha1Comment({
|
commentToUpdate.spec.lastReadTime = new Date().toISOString();
|
||||||
name: commentToUpdate.metadata.name,
|
await apiClient.extension.comment.updatecontentHaloRunV1alpha1Comment({
|
||||||
comment: commentToUpdate,
|
name: commentToUpdate.metadata.name,
|
||||||
});
|
comment: commentToUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emit("reload");
|
emit("reload");
|
||||||
}
|
}
|
||||||
|
@ -202,7 +184,7 @@ const onTriggerReply = (reply: ListedReply) => {
|
||||||
|
|
||||||
const onReplyCreationModalClose = () => {
|
const onReplyCreationModalClose = () => {
|
||||||
selectedReply.value = undefined;
|
selectedReply.value = undefined;
|
||||||
handleFetchReplies({ mute: true });
|
refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Subject ref processing
|
// Subject ref processing
|
||||||
|
@ -413,12 +395,12 @@ const subjectRefResult = computed(() => {
|
||||||
<div
|
<div
|
||||||
class="ml-8 mt-3 divide-y divide-gray-100 rounded-base border-t border-gray-100 pt-3"
|
class="ml-8 mt-3 divide-y divide-gray-100 rounded-base border-t border-gray-100 pt-3"
|
||||||
>
|
>
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!replies.length" appear name="fade">
|
<Transition v-else-if="!replies?.length" appear name="fade">
|
||||||
<VEmpty message="你可以尝试刷新或者创建新回复" title="当前没有回复">
|
<VEmpty message="你可以尝试刷新或者创建新回复" title="当前没有回复">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton @click="handleFetchReplies">刷新</VButton>
|
<VButton @click="refetch()">刷新</VButton>
|
||||||
<VButton type="secondary" @click="replyModal = true">
|
<VButton type="secondary" @click="replyModal = true">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconAddCircle class="h-full w-full" />
|
<IconAddCircle class="h-full w-full" />
|
||||||
|
@ -437,7 +419,7 @@ const subjectRefResult = computed(() => {
|
||||||
:class="{ 'hover:bg-white': showReplies }"
|
:class="{ 'hover:bg-white': showReplies }"
|
||||||
:reply="reply"
|
:reply="reply"
|
||||||
:replies="replies"
|
:replies="replies"
|
||||||
@reload="handleFetchReplies({ mute: true })"
|
@reload="refetch()"
|
||||||
@reply="onTriggerReply"
|
@reply="onTriggerReply"
|
||||||
></ReplyListItem>
|
></ReplyListItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue