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 () => {
await Promise.all( const chunks = chunk(selectedPostNames.value, 5);
selectedPostNames.value.map((name) => {
return coreApiClient.content.post.deletePost({ for (const chunk of chunks) {
name, await Promise.all(
}); chunk.map((name) => {
}) return coreApiClient.content.post.deletePost({
); name,
});
})
);
}
await refetch(); await refetch();
selectedPostNames.value = []; selectedPostNames.value = [];
@ -158,28 +164,33 @@ 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 () => {
await Promise.all( const chunks = chunk(selectedPostNames.value, 5);
selectedPostNames.value.map((name) => {
const isPostExist = posts.value?.some(
(item) => item.post.metadata.name === name
);
if (!isPostExist) { for (const chunk of chunks) {
return Promise.resolve(); await Promise.all(
} chunk.map((name) => {
const isPostExist = posts.value?.some(
(item) => item.post.metadata.name === name
);
if (!isPostExist) {
return Promise.resolve();
}
return coreApiClient.content.post.patchPost({
name: name,
jsonPatchInner: [
{
op: "add",
path: "/spec/deleted",
value: false,
},
],
});
})
);
}
return coreApiClient.content.post.patchPost({
name: name,
jsonPatchInner: [
{
op: "add",
path: "/spec/deleted",
value: false,
},
],
});
})
);
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 () => {
await Promise.all( const chunks = chunk(selectedPostNames.value, 5);
selectedPostNames.value.map((name) => {
return consoleApiClient.content.post.recyclePost({ for (const chunk of chunks) {
name, await Promise.all(
}); chunk.map((name) => {
}) return consoleApiClient.content.post.recyclePost({
); 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,61 +51,66 @@ 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];
const jsonPatchInner: JsonPatchInner[] = [];
if (data.category.enabled) {
jsonPatchInner.push({
op: "add",
path: "/spec/categories",
value: computeArrayPatchValue(
data.category.op,
post.post.spec.categories || [],
data.category.names || []
),
});
}
if (data.tag.enabled) { for (const postChunk of postChunks) {
jsonPatchInner.push({ await Promise.all(
op: "add", postChunk.map((post) => {
path: "/spec/tags", const jsonPatchInner: JsonPatchInner[] = [];
value: computeArrayPatchValue( if (data.category.enabled) {
data.tag.op, jsonPatchInner.push({
post.post.spec.tags || [], op: "add",
data.tag.names || [] path: "/spec/categories",
), value: computeArrayPatchValue(
}); data.category.op,
} post.post.spec.categories || [],
data.category.names || []
),
});
}
if (data.owner.enabled) { if (data.tag.enabled) {
jsonPatchInner.push({ jsonPatchInner.push({
op: "add", op: "add",
path: "/spec/owner", path: "/spec/tags",
value: data.owner.value, value: computeArrayPatchValue(
}); data.tag.op,
} post.post.spec.tags || [],
data.tag.names || []
),
});
}
if (data.visible.enabled) { if (data.owner.enabled) {
jsonPatchInner.push({ jsonPatchInner.push({
op: "add", op: "add",
path: "/spec/visible", path: "/spec/owner",
value: data.visible.value, value: data.owner.value,
}); });
} }
if (data.allowComment.enabled) { if (data.visible.enabled) {
jsonPatchInner.push({ jsonPatchInner.push({
op: "add", op: "add",
path: "/spec/allowComment", path: "/spec/visible",
value: data.allowComment.value, value: data.visible.value,
}); });
} }
await coreApiClient.content.post.patchPost({ if (data.allowComment.enabled) {
name: post.post.metadata.name, jsonPatchInner.push({
jsonPatchInner, op: "add",
}); path: "/spec/allowComment",
value: data.allowComment.value,
});
}
return coreApiClient.content.post.patchPost({
name: post.post.metadata.name,
jsonPatchInner,
});
})
);
} }
Toast.success(t("core.common.toast.save_success")); Toast.success(t("core.common.toast.save_success"));