refactor: add chunked execution mechanism for post batch operations (#7378)

#### What type of PR is this?

/area ui
/kind improvement
/milestone 2.20.x

#### What this PR does / why we need it:

Use `Promise.all` to execute part of the batch operation logic of the post in chunks to optimize the execution performance.

#### Which issue(s) this PR fixes:

Fixes #

#### Does this PR introduce a user-facing change?

```release-note
优化文章部分批量操作的执行性能
```
pull/7386/head
Ryan Wang 2025-04-23 22:17:13 +08:00 committed by GitHub
parent c2819f1f5a
commit 7a315302a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 90 deletions

View File

@ -23,6 +23,7 @@ import {
VStatusDot, VStatusDot,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { chunk } from "lodash-es";
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import PostTag from "./tags/components/PostTag.vue"; import PostTag from "./tags/components/PostTag.vue";
@ -109,13 +110,18 @@ const handleDeletePermanentlyInBatch = async () => {
confirmText: t("core.common.buttons.confirm"), confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"), cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => { onConfirm: async () => {
const chunks = chunk(selectedPostNames.value, 5);
for (const chunk of chunks) {
await Promise.all( await Promise.all(
selectedPostNames.value.map((name) => { chunk.map((name) => {
return coreApiClient.content.post.deletePost({ return coreApiClient.content.post.deletePost({
name, name,
}); });
}) })
); );
}
await refetch(); await refetch();
selectedPostNames.value = []; selectedPostNames.value = [];
@ -158,8 +164,11 @@ const handleRecoveryInBatch = async () => {
confirmText: t("core.common.buttons.confirm"), confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"), cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => { onConfirm: async () => {
const chunks = chunk(selectedPostNames.value, 5);
for (const chunk of chunks) {
await Promise.all( await Promise.all(
selectedPostNames.value.map((name) => { chunk.map((name) => {
const isPostExist = posts.value?.some( const isPostExist = posts.value?.some(
(item) => item.post.metadata.name === name (item) => item.post.metadata.name === name
); );
@ -180,6 +189,8 @@ const handleRecoveryInBatch = async () => {
}); });
}) })
); );
}
await refetch(); await refetch();
selectedPostNames.value = []; selectedPostNames.value = [];

View File

@ -23,6 +23,7 @@ import {
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { chunk } from "lodash-es";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { computed, provide, ref, watch } from "vue"; import { computed, provide, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
@ -266,13 +267,18 @@ const handleDeleteInBatch = async () => {
confirmText: t("core.common.buttons.confirm"), confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"), cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => { onConfirm: async () => {
const chunks = chunk(selectedPostNames.value, 5);
for (const chunk of chunks) {
await Promise.all( await Promise.all(
selectedPostNames.value.map((name) => { chunk.map((name) => {
return consoleApiClient.content.post.recyclePost({ return consoleApiClient.content.post.recyclePost({
name, name,
}); });
}) })
); );
}
await refetch(); await refetch();
selectedPostNames.value = []; selectedPostNames.value = [];
@ -288,9 +294,14 @@ const handlePublishInBatch = async () => {
confirmText: t("core.common.buttons.confirm"), confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"), cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => { onConfirm: async () => {
for (const i in selectedPostNames.value) { const chunks = chunk(selectedPostNames.value, 5);
const name = selectedPostNames.value[i];
await consoleApiClient.content.post.publishPost({ name }); for (const chunk of chunks) {
await Promise.all(
chunk.map((name) => {
return consoleApiClient.content.post.publishPost({ name });
})
);
} }
await refetch(); await refetch();
@ -308,9 +319,14 @@ const handleCancelPublishInBatch = async () => {
confirmText: t("core.common.buttons.confirm"), confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"), cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => { onConfirm: async () => {
for (const i in selectedPostNames.value) { const chunks = chunk(selectedPostNames.value, 5);
const name = selectedPostNames.value[i];
await consoleApiClient.content.post.unpublishPost({ name }); for (const chunk of chunks) {
await Promise.all(
chunk.map((name) => {
return consoleApiClient.content.post.unpublishPost({ name });
})
);
} }
await refetch(); await refetch();

View File

@ -6,6 +6,7 @@ import {
} from "@halo-dev/api-client"; } from "@halo-dev/api-client";
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components"; import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
import { useMutation, useQueryClient } from "@tanstack/vue-query"; import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { chunk } from "lodash-es";
import { ref } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
@ -50,8 +51,11 @@ const modal = ref<InstanceType<typeof VModal> | null>(null);
const { mutate, isLoading } = useMutation({ const { mutate, isLoading } = useMutation({
mutationKey: ["batch-update-posts"], mutationKey: ["batch-update-posts"],
mutationFn: async ({ data }: { data: FormData }) => { mutationFn: async ({ data }: { data: FormData }) => {
for (const key in props.posts) { const postChunks = chunk(props.posts, 5);
const post = props.posts[key];
for (const postChunk of postChunks) {
await Promise.all(
postChunk.map((post) => {
const jsonPatchInner: JsonPatchInner[] = []; const jsonPatchInner: JsonPatchInner[] = [];
if (data.category.enabled) { if (data.category.enabled) {
jsonPatchInner.push({ jsonPatchInner.push({
@ -101,10 +105,12 @@ const { mutate, isLoading } = useMutation({
}); });
} }
await coreApiClient.content.post.patchPost({ return coreApiClient.content.post.patchPost({
name: post.post.metadata.name, name: post.post.metadata.name,
jsonPatchInner, jsonPatchInner,
}); });
})
);
} }
Toast.success(t("core.common.toast.save_success")); Toast.success(t("core.common.toast.save_success"));