mirror of https://github.com/halo-dev/halo-admin
refactor: use tanstack query to refactor post-related fetching (#879)
#### 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/883/head
parent
5a27f56b89
commit
66a626c916
|
@ -20,9 +20,7 @@ const emit = defineEmits<{
|
||||||
(event: "select", category?: Category): void;
|
(event: "select", category?: Category): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { categories, handleFetchCategories } = usePostCategory({
|
const { categories } = usePostCategory();
|
||||||
fetchOnMounted: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSelect = (category: Category) => {
|
const handleSelect = (category: Category) => {
|
||||||
if (
|
if (
|
||||||
|
@ -39,7 +37,6 @@ const handleSelect = (category: Category) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function onDropdownShow() {
|
function onDropdownShow() {
|
||||||
handleFetchCategories();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setFocus("categoryDropdownSelectorInput");
|
setFocus("categoryDropdownSelectorInput");
|
||||||
}, 200);
|
}, 200);
|
||||||
|
@ -53,11 +50,14 @@ let fuse: Fuse<Category> | undefined = undefined;
|
||||||
watch(
|
watch(
|
||||||
() => categories.value,
|
() => categories.value,
|
||||||
() => {
|
() => {
|
||||||
fuse = new Fuse(categories.value, {
|
fuse = new Fuse(categories.value || [], {
|
||||||
keys: ["spec.displayName", "metadata.name"],
|
keys: ["spec.displayName", "metadata.name"],
|
||||||
useExtendedSearch: true,
|
useExtendedSearch: true,
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ const emit = defineEmits<{
|
||||||
(event: "select", tag?: Tag): void;
|
(event: "select", tag?: Tag): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { tags, handleFetchTags } = usePostTag({ fetchOnMounted: false });
|
const { tags } = usePostTag();
|
||||||
|
|
||||||
const handleSelect = (tag: Tag) => {
|
const handleSelect = (tag: Tag) => {
|
||||||
if (props.selected && tag.metadata.name === props.selected.metadata.name) {
|
if (props.selected && tag.metadata.name === props.selected.metadata.name) {
|
||||||
|
@ -35,7 +35,6 @@ const handleSelect = (tag: Tag) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function onDropdownShow() {
|
function onDropdownShow() {
|
||||||
handleFetchTags();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setFocus("tagDropdownSelectorInput");
|
setFocus("tagDropdownSelectorInput");
|
||||||
}, 200);
|
}, 200);
|
||||||
|
@ -49,11 +48,14 @@ let fuse: Fuse<Tag> | undefined = undefined;
|
||||||
watch(
|
watch(
|
||||||
() => tags.value,
|
() => tags.value,
|
||||||
() => {
|
() => {
|
||||||
fuse = new Fuse(tags.value, {
|
fuse = new Fuse(tags.value || [], {
|
||||||
keys: ["spec.displayName", "metadata.name", "spec.email"],
|
keys: ["spec.displayName", "metadata.name", "spec.email"],
|
||||||
useExtendedSearch: true,
|
useExtendedSearch: true,
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -35,9 +35,7 @@ const multiple = computed(() => {
|
||||||
return multiple === "true";
|
return multiple === "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
const { categories, categoriesTree, handleFetchCategories } = usePostCategory({
|
const { categories, categoriesTree, handleFetchCategories } = usePostCategory();
|
||||||
fetchOnMounted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
provide<Ref<CategoryTree[]>>("categoriesTree", categoriesTree);
|
provide<Ref<CategoryTree[]>>("categoriesTree", categoriesTree);
|
||||||
|
|
||||||
|
@ -69,7 +67,7 @@ const searchResults = computed(() => {
|
||||||
watch(
|
watch(
|
||||||
() => searchResults.value,
|
() => searchResults.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (value?.length > 0 && text.value) {
|
if (value?.length && text.value) {
|
||||||
selectedCategory.value = value[0];
|
selectedCategory.value = value[0];
|
||||||
scrollToSelected();
|
scrollToSelected();
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,11 +79,14 @@ watch(
|
||||||
watch(
|
watch(
|
||||||
() => categories.value,
|
() => categories.value,
|
||||||
() => {
|
() => {
|
||||||
fuse = new Fuse(categories.value, {
|
fuse = new Fuse(categories.value || [], {
|
||||||
keys: ["spec.displayName", "spec.slug"],
|
keys: ["spec.displayName", "spec.slug"],
|
||||||
useExtendedSearch: true,
|
useExtendedSearch: true,
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -94,14 +95,14 @@ const selectedCategories = computed(() => {
|
||||||
const currentValue = props.context._value || [];
|
const currentValue = props.context._value || [];
|
||||||
return currentValue
|
return currentValue
|
||||||
.map((categoryName): Category | undefined => {
|
.map((categoryName): Category | undefined => {
|
||||||
return categories.value.find(
|
return categories.value?.find(
|
||||||
(category) => category.metadata.name === categoryName
|
(category) => category.metadata.name === categoryName
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.filter(Boolean) as Category[];
|
.filter(Boolean) as Category[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = categories.value.find(
|
const category = categories.value?.find(
|
||||||
(category) => category.metadata.name === props.context._value
|
(category) => category.metadata.name === props.context._value
|
||||||
);
|
);
|
||||||
return [category].filter(Boolean) as Category[];
|
return [category].filter(Boolean) as Category[];
|
||||||
|
@ -140,6 +141,8 @@ const handleSelect = (category: CategoryTree | Category) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (!searchResults.value) return;
|
||||||
|
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -217,7 +220,7 @@ const handleCreateCategory = async () => {
|
||||||
description: "",
|
description: "",
|
||||||
cover: "",
|
cover: "",
|
||||||
template: "",
|
template: "",
|
||||||
priority: categories.value.length + 1,
|
priority: categories.value?.length || 0 + 1,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
apiVersion: "content.halo.run/v1alpha1",
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
|
@ -286,7 +289,7 @@ const handleDelete = () => {
|
||||||
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
||||||
<ul class="p-1">
|
<ul class="p-1">
|
||||||
<li
|
<li
|
||||||
v-if="text.trim() && searchResults.length <= 0"
|
v-if="text.trim() && !searchResults?.length"
|
||||||
v-permission="['system:posts:manage']"
|
v-permission="['system:posts:manage']"
|
||||||
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||||
>
|
>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const label = computed(() => {
|
||||||
props.category.metadata.name
|
props.category.metadata.name
|
||||||
);
|
);
|
||||||
return categories
|
return categories
|
||||||
.map((category: CategoryTree) => category.spec.displayName)
|
?.map((category: CategoryTree) => category.spec.displayName)
|
||||||
.join(" / ");
|
.join(" / ");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -33,7 +33,7 @@ const label = computed(() => {
|
||||||
props.category.metadata.name
|
props.category.metadata.name
|
||||||
);
|
);
|
||||||
return categories
|
return categories
|
||||||
.map((category: CategoryTree) => category.spec.displayName)
|
?.map((category: CategoryTree) => category.spec.displayName)
|
||||||
.join(" / ");
|
.join(" / ");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import type { FormKitFrameworkContext } from "@formkit/core";
|
import type { FormKitFrameworkContext } from "@formkit/core";
|
||||||
import type { Tag } from "@halo-dev/api-client";
|
import type { Tag } from "@halo-dev/api-client";
|
||||||
import { computed, onMounted, ref, watch, type PropType } from "vue";
|
import { computed, ref, watch, type PropType } from "vue";
|
||||||
import PostTag from "@/modules/contents/posts/tags/components/PostTag.vue";
|
import PostTag from "@/modules/contents/posts/tags/components/PostTag.vue";
|
||||||
import {
|
import {
|
||||||
IconCheckboxCircle,
|
IconCheckboxCircle,
|
||||||
|
@ -13,6 +13,7 @@ import { onClickOutside } from "@vueuse/core";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { slugify } from "transliteration";
|
import { slugify } from "transliteration";
|
||||||
|
import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
@ -36,7 +37,6 @@ const multiple = computed(() => {
|
||||||
return multiple === "true";
|
return multiple === "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
const postTags = ref<Tag[]>([] as Tag[]);
|
|
||||||
const selectedTag = ref<Tag>();
|
const selectedTag = ref<Tag>();
|
||||||
const dropdownVisible = ref(false);
|
const dropdownVisible = ref(false);
|
||||||
const text = ref("");
|
const text = ref("");
|
||||||
|
@ -46,8 +46,9 @@ onClickOutside(wrapperRef, () => {
|
||||||
dropdownVisible.value = false;
|
dropdownVisible.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// search
|
const { tags: postTags, handleFetchTags } = usePostTag();
|
||||||
|
|
||||||
|
// search
|
||||||
let fuse: Fuse<Tag> | undefined = undefined;
|
let fuse: Fuse<Tag> | undefined = undefined;
|
||||||
|
|
||||||
const searchResults = computed(() => {
|
const searchResults = computed(() => {
|
||||||
|
@ -61,7 +62,7 @@ const searchResults = computed(() => {
|
||||||
watch(
|
watch(
|
||||||
() => searchResults.value,
|
() => searchResults.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (value?.length > 0 && text.value) {
|
if (value?.length && text.value) {
|
||||||
selectedTag.value = value[0];
|
selectedTag.value = value[0];
|
||||||
scrollToSelected();
|
scrollToSelected();
|
||||||
} else {
|
} else {
|
||||||
|
@ -70,32 +71,31 @@ watch(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFetchTags = async () => {
|
watch(
|
||||||
const { data } = await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
|
() => postTags.value,
|
||||||
page: 0,
|
() => {
|
||||||
size: 0,
|
fuse = new Fuse(postTags.value || [], {
|
||||||
});
|
keys: ["spec.displayName", "metadata.name", "spec.email"],
|
||||||
|
useExtendedSearch: true,
|
||||||
postTags.value = data.items;
|
threshold: 0.2,
|
||||||
|
});
|
||||||
fuse = new Fuse(data.items, {
|
},
|
||||||
keys: ["spec.displayName", "spec.slug"],
|
{
|
||||||
useExtendedSearch: true,
|
immediate: true,
|
||||||
threshold: 0.2,
|
}
|
||||||
});
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const selectedTags = computed(() => {
|
const selectedTags = computed(() => {
|
||||||
if (multiple.value) {
|
if (multiple.value) {
|
||||||
const selectedTagNames = (props.context._value as string[]) || [];
|
const selectedTagNames = (props.context._value as string[]) || [];
|
||||||
return selectedTagNames
|
return selectedTagNames
|
||||||
.map((tagName): Tag | undefined => {
|
.map((tagName): Tag | undefined => {
|
||||||
return postTags.value.find((tag) => tag.metadata.name === tagName);
|
return postTags.value?.find((tag) => tag.metadata.name === tagName);
|
||||||
})
|
})
|
||||||
.filter(Boolean) as Tag[];
|
.filter(Boolean) as Tag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = postTags.value.find(
|
const tag = postTags.value?.find(
|
||||||
(tag) => tag.metadata.name === props.context._value
|
(tag) => tag.metadata.name === props.context._value
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -129,6 +129,8 @@ const handleSelect = (tag: Tag) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (!searchResults.value) return;
|
||||||
|
|
||||||
if (e.key === "ArrowDown") {
|
if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -234,8 +236,6 @@ const handleDelete = () => {
|
||||||
props.context.node.input("");
|
props.context.node.input("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(handleFetchTags);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -279,7 +279,7 @@ onMounted(handleFetchTags);
|
||||||
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
||||||
<ul class="p-1">
|
<ul class="p-1">
|
||||||
<li
|
<li
|
||||||
v-if="text.trim() && searchResults.length <= 0"
|
v-if="text.trim() && !searchResults?.length"
|
||||||
v-permission="['system:posts:manage']"
|
v-permission="['system:posts:manage']"
|
||||||
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||||
@click="handleCreateTag"
|
@click="handleCreateTag"
|
||||||
|
|
|
@ -18,86 +18,55 @@ import {
|
||||||
Toast,
|
Toast,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import PostTag from "./tags/components/PostTag.vue";
|
import PostTag from "./tags/components/PostTag.vue";
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import type { ListedPostList, Post } from "@halo-dev/api-client";
|
import type { ListedPost, Post } from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const posts = ref<ListedPostList>({
|
|
||||||
page: 1,
|
|
||||||
size: 50,
|
|
||||||
total: 0,
|
|
||||||
items: [],
|
|
||||||
first: true,
|
|
||||||
last: false,
|
|
||||||
hasNext: false,
|
|
||||||
hasPrevious: false,
|
|
||||||
totalPages: 0,
|
|
||||||
});
|
|
||||||
const loading = ref(false);
|
|
||||||
const checkedAll = ref(false);
|
const checkedAll = ref(false);
|
||||||
const selectedPostNames = ref<string[]>([]);
|
const selectedPostNames = ref<string[]>([]);
|
||||||
const refreshInterval = ref();
|
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
|
|
||||||
const handleFetchPosts = async (page?: number) => {
|
const page = ref(1);
|
||||||
try {
|
const size = ref(20);
|
||||||
clearInterval(refreshInterval.value);
|
const total = ref(0);
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
if (page) {
|
|
||||||
posts.value.page = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: posts,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
refetch,
|
||||||
|
} = useQuery<ListedPost[]>({
|
||||||
|
queryKey: ["deleted-posts", page, size, keyword],
|
||||||
|
queryFn: async () => {
|
||||||
const { data } = await apiClient.post.listPosts({
|
const { data } = await apiClient.post.listPosts({
|
||||||
labelSelector: [`content.halo.run/deleted=true`],
|
labelSelector: [`content.halo.run/deleted=true`],
|
||||||
page: posts.value.page,
|
page: page.value,
|
||||||
size: posts.value.size,
|
size: size.value,
|
||||||
keyword: keyword.value,
|
keyword: keyword.value,
|
||||||
});
|
});
|
||||||
posts.value = data;
|
|
||||||
|
|
||||||
const deletedPosts = posts.value.items.filter(
|
total.value = data.total;
|
||||||
|
|
||||||
|
return data.items;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval: (data) => {
|
||||||
|
const deletingPosts = data?.filter(
|
||||||
(post) =>
|
(post) =>
|
||||||
!!post.post.metadata.deletionTimestamp || !post.post.spec.deleted
|
!!post.post.metadata.deletionTimestamp || !post.post.spec.deleted
|
||||||
);
|
);
|
||||||
|
return deletingPosts?.length ? 3000 : false;
|
||||||
if (deletedPosts.length) {
|
},
|
||||||
refreshInterval.value = setInterval(() => {
|
|
||||||
handleFetchPosts();
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch deleted posts", e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePaginationChange = ({
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
}: {
|
|
||||||
page: number;
|
|
||||||
size: number;
|
|
||||||
}) => {
|
|
||||||
posts.value.page = page;
|
|
||||||
posts.value.size = size;
|
|
||||||
handleFetchPosts();
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkSelection = (post: Post) => {
|
const checkSelection = (post: Post) => {
|
||||||
return selectedPostNames.value.includes(post.metadata.name);
|
return selectedPostNames.value.includes(post.metadata.name);
|
||||||
};
|
};
|
||||||
|
@ -107,7 +76,7 @@ const handleCheckAllChange = (e: Event) => {
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
selectedPostNames.value =
|
selectedPostNames.value =
|
||||||
posts.value.items.map((post) => {
|
posts.value?.map((post) => {
|
||||||
return post.post.metadata.name;
|
return post.post.metadata.name;
|
||||||
}) || [];
|
}) || [];
|
||||||
} else {
|
} else {
|
||||||
|
@ -124,7 +93,7 @@ const handleDeletePermanently = async (post: Post) => {
|
||||||
await apiClient.extension.post.deletecontentHaloRunV1alpha1Post({
|
await apiClient.extension.post.deletecontentHaloRunV1alpha1Post({
|
||||||
name: post.metadata.name,
|
name: post.metadata.name,
|
||||||
});
|
});
|
||||||
await handleFetchPosts();
|
await refetch();
|
||||||
|
|
||||||
Toast.success("删除成功");
|
Toast.success("删除成功");
|
||||||
},
|
},
|
||||||
|
@ -144,7 +113,7 @@ const handleDeletePermanentlyInBatch = async () => {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await handleFetchPosts();
|
await refetch();
|
||||||
selectedPostNames.value = [];
|
selectedPostNames.value = [];
|
||||||
|
|
||||||
Toast.success("删除成功");
|
Toast.success("删除成功");
|
||||||
|
@ -163,7 +132,8 @@ const handleRecovery = async (post: Post) => {
|
||||||
name: postToUpdate.metadata.name,
|
name: postToUpdate.metadata.name,
|
||||||
post: postToUpdate,
|
post: postToUpdate,
|
||||||
});
|
});
|
||||||
await handleFetchPosts();
|
|
||||||
|
await refetch();
|
||||||
|
|
||||||
Toast.success("恢复成功");
|
Toast.success("恢复成功");
|
||||||
},
|
},
|
||||||
|
@ -177,7 +147,7 @@ const handleRecoveryInBatch = async () => {
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
selectedPostNames.value.map((name) => {
|
selectedPostNames.value.map((name) => {
|
||||||
const post = posts.value.items.find(
|
const post = posts.value?.find(
|
||||||
(item) => item.post.metadata.name === name
|
(item) => item.post.metadata.name === name
|
||||||
)?.post;
|
)?.post;
|
||||||
|
|
||||||
|
@ -185,14 +155,19 @@ const handleRecoveryInBatch = async () => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
post.spec.deleted = false;
|
|
||||||
return apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
|
return apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
|
||||||
name: post.metadata.name,
|
name: post.metadata.name,
|
||||||
post: post,
|
post: {
|
||||||
|
...post,
|
||||||
|
spec: {
|
||||||
|
...post.spec,
|
||||||
|
deleted: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await handleFetchPosts();
|
await refetch();
|
||||||
selectedPostNames.value = [];
|
selectedPostNames.value = [];
|
||||||
|
|
||||||
Toast.success("恢复成功");
|
Toast.success("恢复成功");
|
||||||
|
@ -201,11 +176,7 @@ const handleRecoveryInBatch = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(selectedPostNames, (newValue) => {
|
watch(selectedPostNames, (newValue) => {
|
||||||
checkedAll.value = newValue.length === posts.value.items?.length;
|
checkedAll.value = newValue.length === posts.value?.length;
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
handleFetchPosts();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleKeywordChange() {
|
function handleKeywordChange() {
|
||||||
|
@ -213,12 +184,12 @@ function handleKeywordChange() {
|
||||||
if (keywordNode) {
|
if (keywordNode) {
|
||||||
keyword.value = keywordNode._value as string;
|
keyword.value = keywordNode._value as string;
|
||||||
}
|
}
|
||||||
handleFetchPosts(1);
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClearKeyword() {
|
function handleClearKeyword() {
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
handleFetchPosts(1);
|
page.value = 1;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
@ -294,10 +265,10 @@ function handleClearKeyword() {
|
||||||
<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="handleFetchPosts()"
|
@click="refetch()"
|
||||||
>
|
>
|
||||||
<IconRefreshLine
|
<IconRefreshLine
|
||||||
: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>
|
||||||
|
@ -308,16 +279,16 @@ function handleClearKeyword() {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
|
|
||||||
<Transition v-else-if="!posts.items.length" appear name="fade">
|
<Transition v-else-if="!posts?.length" appear name="fade">
|
||||||
<VEmpty
|
<VEmpty
|
||||||
message="你可以尝试刷新或者返回文章管理"
|
message="你可以尝试刷新或者返回文章管理"
|
||||||
title="没有文章被放入回收站"
|
title="没有文章被放入回收站"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton @click="handleFetchPosts">刷新</VButton>
|
<VButton @click="refetch">刷新</VButton>
|
||||||
<VButton :route="{ name: 'Posts' }" type="primary">
|
<VButton :route="{ name: 'Posts' }" type="primary">
|
||||||
返回
|
返回
|
||||||
</VButton>
|
</VButton>
|
||||||
|
@ -331,7 +302,7 @@ function handleClearKeyword() {
|
||||||
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 v-for="(post, index) in posts.items" :key="index">
|
<li v-for="(post, index) in posts" :key="index">
|
||||||
<VEntity :is-selected="checkSelection(post.post)">
|
<VEntity :is-selected="checkSelection(post.post)">
|
||||||
<template
|
<template
|
||||||
v-if="currentUserHasPermission(['system:posts:manage'])"
|
v-if="currentUserHasPermission(['system:posts:manage'])"
|
||||||
|
@ -452,11 +423,10 @@ function handleClearKeyword() {
|
||||||
<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="posts.page"
|
v-model:page="page"
|
||||||
:size="posts.size"
|
v-model:size="size"
|
||||||
:total="posts.total"
|
:total="total"
|
||||||
:size-options="[20, 30, 50, 100]"
|
:size-options="[20, 30, 50, 100]"
|
||||||
@change="handlePaginationChange"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -28,282 +28,32 @@ import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSel
|
||||||
import CategoryDropdownSelector from "@/components/dropdown-selector/CategoryDropdownSelector.vue";
|
import CategoryDropdownSelector from "@/components/dropdown-selector/CategoryDropdownSelector.vue";
|
||||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||||
import PostTag from "../posts/tags/components/PostTag.vue";
|
import PostTag from "../posts/tags/components/PostTag.vue";
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import type {
|
import type {
|
||||||
User,
|
User,
|
||||||
Category,
|
Category,
|
||||||
ListedPostList,
|
|
||||||
Post,
|
Post,
|
||||||
Tag,
|
Tag,
|
||||||
|
ListedPost,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const posts = ref<ListedPostList>({
|
|
||||||
page: 1,
|
|
||||||
size: 20,
|
|
||||||
total: 0,
|
|
||||||
items: [],
|
|
||||||
first: true,
|
|
||||||
last: false,
|
|
||||||
hasNext: false,
|
|
||||||
hasPrevious: false,
|
|
||||||
totalPages: 0,
|
|
||||||
});
|
|
||||||
const loading = ref(false);
|
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const selectedPost = ref<Post>();
|
const selectedPost = ref<Post>();
|
||||||
const checkedAll = ref(false);
|
const checkedAll = ref(false);
|
||||||
const selectedPostNames = ref<string[]>([]);
|
const selectedPostNames = ref<string[]>([]);
|
||||||
const refreshInterval = ref();
|
|
||||||
|
|
||||||
const handleFetchPosts = async (options?: {
|
|
||||||
mute?: boolean;
|
|
||||||
page?: number;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
|
|
||||||
if (!options?.mute) {
|
|
||||||
loading.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let categories: string[] | undefined;
|
|
||||||
let tags: string[] | undefined;
|
|
||||||
let contributors: string[] | undefined;
|
|
||||||
const labelSelector: string[] = ["content.halo.run/deleted=false"];
|
|
||||||
|
|
||||||
if (selectedCategory.value) {
|
|
||||||
categories = [
|
|
||||||
selectedCategory.value.metadata.name,
|
|
||||||
selectedCategory.value.metadata.name,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTag.value) {
|
|
||||||
tags = [selectedTag.value.metadata.name];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedContributor.value) {
|
|
||||||
contributors = [selectedContributor.value.metadata.name];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedPublishStatusItem.value.value !== undefined) {
|
|
||||||
labelSelector.push(
|
|
||||||
`${postLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.page) {
|
|
||||||
posts.value.page = options.page;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await apiClient.post.listPosts({
|
|
||||||
labelSelector,
|
|
||||||
page: posts.value.page,
|
|
||||||
size: posts.value.size,
|
|
||||||
visible: selectedVisibleItem.value?.value,
|
|
||||||
sort: selectedSortItem.value?.sort,
|
|
||||||
sortOrder: selectedSortItem.value?.sortOrder,
|
|
||||||
keyword: keyword.value,
|
|
||||||
category: categories,
|
|
||||||
tag: tags,
|
|
||||||
contributor: contributors,
|
|
||||||
});
|
|
||||||
posts.value = data;
|
|
||||||
|
|
||||||
// When an post is in the process of deleting or publishing, the list needs to be refreshed regularly
|
|
||||||
const abnormalPosts = posts.value.items.filter((post) => {
|
|
||||||
const { spec, metadata, status } = post.post;
|
|
||||||
return (
|
|
||||||
spec.deleted ||
|
|
||||||
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
|
|
||||||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (abnormalPosts.length) {
|
|
||||||
refreshInterval.value = setInterval(() => {
|
|
||||||
handleFetchPosts({ mute: true });
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch posts", e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePaginationChange = ({
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
}: {
|
|
||||||
page: number;
|
|
||||||
size: number;
|
|
||||||
}) => {
|
|
||||||
posts.value.page = page;
|
|
||||||
posts.value.size = size;
|
|
||||||
handleFetchPosts();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenSettingModal = async (post: Post) => {
|
|
||||||
const { data } = await apiClient.extension.post.getcontentHaloRunV1alpha1Post(
|
|
||||||
{
|
|
||||||
name: post.metadata.name,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
selectedPost.value = data;
|
|
||||||
settingModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSettingModalClose = () => {
|
|
||||||
selectedPost.value = undefined;
|
|
||||||
handleFetchPosts({ mute: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectPrevious = async () => {
|
|
||||||
const { items, hasPrevious } = posts.value;
|
|
||||||
const index = items.findIndex(
|
|
||||||
(post) => post.post.metadata.name === selectedPost.value?.metadata.name
|
|
||||||
);
|
|
||||||
if (index > 0) {
|
|
||||||
const { data } =
|
|
||||||
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
|
||||||
name: items[index - 1].post.metadata.name,
|
|
||||||
});
|
|
||||||
selectedPost.value = data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (index === 0 && hasPrevious) {
|
|
||||||
posts.value.page--;
|
|
||||||
await handleFetchPosts();
|
|
||||||
selectedPost.value = posts.value.items[posts.value.items.length - 1].post;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectNext = async () => {
|
|
||||||
const { items, hasNext } = posts.value;
|
|
||||||
const index = items.findIndex(
|
|
||||||
(post) => post.post.metadata.name === selectedPost.value?.metadata.name
|
|
||||||
);
|
|
||||||
if (index < items.length - 1) {
|
|
||||||
const { data } =
|
|
||||||
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
|
||||||
name: items[index + 1].post.metadata.name,
|
|
||||||
});
|
|
||||||
selectedPost.value = data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (index === items.length - 1 && hasNext) {
|
|
||||||
posts.value.page++;
|
|
||||||
await handleFetchPosts();
|
|
||||||
selectedPost.value = posts.value.items[0].post;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkSelection = (post: Post) => {
|
|
||||||
return (
|
|
||||||
post.metadata.name === selectedPost.value?.metadata.name ||
|
|
||||||
selectedPostNames.value.includes(post.metadata.name)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPublishStatus = (post: Post) => {
|
|
||||||
const { labels } = post.metadata;
|
|
||||||
return labels?.[postLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPublishing = (post: Post) => {
|
|
||||||
const { spec, status, metadata } = post;
|
|
||||||
return (
|
|
||||||
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
|
|
||||||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckAllChange = (e: Event) => {
|
|
||||||
const { checked } = e.target as HTMLInputElement;
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
selectedPostNames.value =
|
|
||||||
posts.value.items.map((post) => {
|
|
||||||
return post.post.metadata.name;
|
|
||||||
}) || [];
|
|
||||||
} else {
|
|
||||||
selectedPostNames.value = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (post: Post) => {
|
|
||||||
Dialog.warning({
|
|
||||||
title: "确定要删除该文章吗?",
|
|
||||||
description: "该操作会将文章放入回收站,后续可以从回收站恢复",
|
|
||||||
confirmType: "danger",
|
|
||||||
onConfirm: async () => {
|
|
||||||
await apiClient.post.recyclePost({
|
|
||||||
name: post.metadata.name,
|
|
||||||
});
|
|
||||||
await handleFetchPosts();
|
|
||||||
|
|
||||||
Toast.success("删除成功");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteInBatch = async () => {
|
|
||||||
Dialog.warning({
|
|
||||||
title: "确定要删除选中的文章吗?",
|
|
||||||
description: "该操作会将文章放入回收站,后续可以从回收站恢复",
|
|
||||||
confirmType: "danger",
|
|
||||||
onConfirm: async () => {
|
|
||||||
await Promise.all(
|
|
||||||
selectedPostNames.value.map((name) => {
|
|
||||||
const post = posts.value.items.find(
|
|
||||||
(item) => item.post.metadata.name === name
|
|
||||||
)?.post;
|
|
||||||
|
|
||||||
if (!post) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
post.spec.deleted = true;
|
|
||||||
return apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
|
|
||||||
name: post.metadata.name,
|
|
||||||
post: post,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await handleFetchPosts();
|
|
||||||
selectedPostNames.value = [];
|
|
||||||
|
|
||||||
Toast.success("删除成功");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(selectedPostNames, (newValue) => {
|
|
||||||
checkedAll.value = newValue.length === posts.value.items?.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
handleFetchPosts();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
|
|
||||||
interface VisibleItem {
|
interface VisibleItem {
|
||||||
label: string;
|
label: string;
|
||||||
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
||||||
|
@ -388,32 +138,32 @@ const keyword = ref("");
|
||||||
|
|
||||||
function handleVisibleItemChange(visibleItem: VisibleItem) {
|
function handleVisibleItemChange(visibleItem: VisibleItem) {
|
||||||
selectedVisibleItem.value = visibleItem;
|
selectedVisibleItem.value = visibleItem;
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePublishStatusItemChange(publishStatusItem: PublishStatuItem) {
|
function handlePublishStatusItemChange(publishStatusItem: PublishStatuItem) {
|
||||||
selectedPublishStatusItem.value = publishStatusItem;
|
selectedPublishStatusItem.value = publishStatusItem;
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSortItemChange(sortItem?: SortItem) {
|
function handleSortItemChange(sortItem?: SortItem) {
|
||||||
selectedSortItem.value = sortItem;
|
selectedSortItem.value = sortItem;
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCategoryChange(category?: Category) {
|
function handleCategoryChange(category?: Category) {
|
||||||
selectedCategory.value = category;
|
selectedCategory.value = category;
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTagChange(tag?: Tag) {
|
function handleTagChange(tag?: Tag) {
|
||||||
selectedTag.value = tag;
|
selectedTag.value = tag;
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContributorChange(user?: User) {
|
function handleContributorChange(user?: User) {
|
||||||
selectedContributor.value = user;
|
selectedContributor.value = user;
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeywordChange() {
|
function handleKeywordChange() {
|
||||||
|
@ -421,12 +171,12 @@ function handleKeywordChange() {
|
||||||
if (keywordNode) {
|
if (keywordNode) {
|
||||||
keyword.value = keywordNode._value as string;
|
keyword.value = keywordNode._value as string;
|
||||||
}
|
}
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClearKeyword() {
|
function handleClearKeyword() {
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClearFilters() {
|
function handleClearFilters() {
|
||||||
|
@ -437,7 +187,7 @@ function handleClearFilters() {
|
||||||
selectedTag.value = undefined;
|
selectedTag.value = undefined;
|
||||||
selectedContributor.value = undefined;
|
selectedContributor.value = undefined;
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
handleFetchPosts({ page: 1 });
|
page.value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFilters = computed(() => {
|
const hasFilters = computed(() => {
|
||||||
|
@ -451,6 +201,220 @@ const hasFilters = computed(() => {
|
||||||
keyword.value
|
keyword.value
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const page = ref(1);
|
||||||
|
const size = ref(20);
|
||||||
|
const total = ref(0);
|
||||||
|
const hasPrevious = ref(false);
|
||||||
|
const hasNext = ref(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: posts,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
refetch,
|
||||||
|
} = useQuery<ListedPost[]>({
|
||||||
|
queryKey: [
|
||||||
|
"posts",
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
selectedCategory,
|
||||||
|
selectedTag,
|
||||||
|
selectedContributor,
|
||||||
|
selectedPublishStatusItem,
|
||||||
|
selectedVisibleItem,
|
||||||
|
selectedSortItem,
|
||||||
|
keyword,
|
||||||
|
],
|
||||||
|
queryFn: async () => {
|
||||||
|
let categories: string[] | undefined;
|
||||||
|
let tags: string[] | undefined;
|
||||||
|
let contributors: string[] | undefined;
|
||||||
|
const labelSelector: string[] = ["content.halo.run/deleted=false"];
|
||||||
|
|
||||||
|
if (selectedCategory.value) {
|
||||||
|
categories = [selectedCategory.value.metadata.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTag.value) {
|
||||||
|
tags = [selectedTag.value.metadata.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedContributor.value) {
|
||||||
|
contributors = [selectedContributor.value.metadata.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPublishStatusItem.value.value !== undefined) {
|
||||||
|
labelSelector.push(
|
||||||
|
`${postLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await apiClient.post.listPosts({
|
||||||
|
labelSelector,
|
||||||
|
page: page.value,
|
||||||
|
size: size.value,
|
||||||
|
visible: selectedVisibleItem.value?.value,
|
||||||
|
sort: selectedSortItem.value?.sort,
|
||||||
|
sortOrder: selectedSortItem.value?.sortOrder,
|
||||||
|
keyword: keyword.value,
|
||||||
|
category: categories,
|
||||||
|
tag: tags,
|
||||||
|
contributor: contributors,
|
||||||
|
});
|
||||||
|
|
||||||
|
total.value = data.total;
|
||||||
|
hasNext.value = data.hasNext;
|
||||||
|
hasPrevious.value = data.hasPrevious;
|
||||||
|
|
||||||
|
return data.items;
|
||||||
|
},
|
||||||
|
refetchInterval: (data) => {
|
||||||
|
const abnormalPosts = data?.filter((post) => {
|
||||||
|
const { spec, metadata, status } = post.post;
|
||||||
|
return (
|
||||||
|
spec.deleted ||
|
||||||
|
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
|
||||||
|
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return abnormalPosts?.length ? 3000 : false;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenSettingModal = async (post: Post) => {
|
||||||
|
const { data } = await apiClient.extension.post.getcontentHaloRunV1alpha1Post(
|
||||||
|
{
|
||||||
|
name: post.metadata.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
selectedPost.value = data;
|
||||||
|
settingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSettingModalClose = () => {
|
||||||
|
selectedPost.value = undefined;
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectPrevious = async () => {
|
||||||
|
if (!posts.value) return;
|
||||||
|
|
||||||
|
const index = posts.value.findIndex(
|
||||||
|
(post) => post.post.metadata.name === selectedPost.value?.metadata.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
const { data: previousPost } =
|
||||||
|
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
||||||
|
name: posts.value[index - 1].post.metadata.name,
|
||||||
|
});
|
||||||
|
selectedPost.value = previousPost;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === 0 && hasPrevious) {
|
||||||
|
page.value--;
|
||||||
|
await refetch();
|
||||||
|
selectedPost.value = posts.value[posts.value.length - 1].post;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectNext = async () => {
|
||||||
|
if (!posts.value) return;
|
||||||
|
|
||||||
|
const index = posts.value.findIndex(
|
||||||
|
(post) => post.post.metadata.name === selectedPost.value?.metadata.name
|
||||||
|
);
|
||||||
|
if (index < posts.value.length - 1) {
|
||||||
|
const { data: nextPost } =
|
||||||
|
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
||||||
|
name: posts.value[index + 1].post.metadata.name,
|
||||||
|
});
|
||||||
|
selectedPost.value = nextPost;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === posts.value.length - 1 && hasNext) {
|
||||||
|
page.value++;
|
||||||
|
await refetch();
|
||||||
|
selectedPost.value = posts.value[0].post;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkSelection = (post: Post) => {
|
||||||
|
return (
|
||||||
|
post.metadata.name === selectedPost.value?.metadata.name ||
|
||||||
|
selectedPostNames.value.includes(post.metadata.name)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublishStatus = (post: Post) => {
|
||||||
|
const { labels } = post.metadata;
|
||||||
|
return labels?.[postLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPublishing = (post: Post) => {
|
||||||
|
const { spec, status, metadata } = post;
|
||||||
|
return (
|
||||||
|
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
|
||||||
|
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckAllChange = (e: Event) => {
|
||||||
|
const { checked } = e.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
selectedPostNames.value =
|
||||||
|
posts.value?.map((post) => {
|
||||||
|
return post.post.metadata.name;
|
||||||
|
}) || [];
|
||||||
|
} else {
|
||||||
|
selectedPostNames.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (post: Post) => {
|
||||||
|
Dialog.warning({
|
||||||
|
title: "确定要删除该文章吗?",
|
||||||
|
description: "该操作会将文章放入回收站,后续可以从回收站恢复",
|
||||||
|
confirmType: "danger",
|
||||||
|
onConfirm: async () => {
|
||||||
|
await apiClient.post.recyclePost({
|
||||||
|
name: post.metadata.name,
|
||||||
|
});
|
||||||
|
await refetch();
|
||||||
|
|
||||||
|
Toast.success("删除成功");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteInBatch = async () => {
|
||||||
|
Dialog.warning({
|
||||||
|
title: "确定要删除选中的文章吗?",
|
||||||
|
description: "该操作会将文章放入回收站,后续可以从回收站恢复",
|
||||||
|
confirmType: "danger",
|
||||||
|
onConfirm: async () => {
|
||||||
|
await Promise.all(
|
||||||
|
selectedPostNames.value.map((name) => {
|
||||||
|
return apiClient.post.recyclePost({
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await refetch();
|
||||||
|
selectedPostNames.value = [];
|
||||||
|
|
||||||
|
Toast.success("删除成功");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(selectedPostNames, (newValue) => {
|
||||||
|
checkedAll.value = newValue.length === posts.value?.length;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<PostSettingModal
|
<PostSettingModal
|
||||||
|
@ -708,11 +672,11 @@ const hasFilters = computed(() => {
|
||||||
<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="handleFetchPosts()"
|
@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>
|
||||||
|
@ -722,12 +686,12 @@ const hasFilters = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!posts.items.length" appear name="fade">
|
<Transition v-else-if="!posts?.length" appear name="fade">
|
||||||
<VEmpty message="你可以尝试刷新或者新建文章" title="当前没有文章">
|
<VEmpty message="你可以尝试刷新或者新建文章" title="当前没有文章">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton @click="handleFetchPosts">刷新</VButton>
|
<VButton @click="refetch">刷新</VButton>
|
||||||
<VButton
|
<VButton
|
||||||
v-permission="['system:posts:manage']"
|
v-permission="['system:posts:manage']"
|
||||||
:route="{ name: 'PostEditor' }"
|
:route="{ name: 'PostEditor' }"
|
||||||
|
@ -747,7 +711,7 @@ const hasFilters = computed(() => {
|
||||||
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 v-for="(post, index) in posts.items" :key="index">
|
<li v-for="(post, index) in posts" :key="index">
|
||||||
<VEntity :is-selected="checkSelection(post.post)">
|
<VEntity :is-selected="checkSelection(post.post)">
|
||||||
<template
|
<template
|
||||||
v-if="currentUserHasPermission(['system:posts:manage'])"
|
v-if="currentUserHasPermission(['system:posts:manage'])"
|
||||||
|
@ -928,11 +892,10 @@ const hasFilters = computed(() => {
|
||||||
<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="posts.page"
|
v-model:page="page"
|
||||||
:size="posts.size"
|
v-model:size="size"
|
||||||
:total="posts.total"
|
:total="total"
|
||||||
:size-options="[20, 30, 50, 100]"
|
:size-options="[20, 30, 50, 100]"
|
||||||
@change="handlePaginationChange"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -38,10 +38,10 @@ const selectedCategory = ref<Category | null>(null);
|
||||||
const {
|
const {
|
||||||
categories,
|
categories,
|
||||||
categoriesTree,
|
categoriesTree,
|
||||||
loading,
|
isLoading,
|
||||||
handleFetchCategories,
|
handleFetchCategories,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
} = usePostCategory({ fetchOnMounted: true });
|
} = usePostCategory();
|
||||||
|
|
||||||
const handleUpdateInBatch = useDebounceFn(async () => {
|
const handleUpdateInBatch = useDebounceFn(async () => {
|
||||||
const categoriesTreeToUpdate = resetCategoriesTreePriority(
|
const categoriesTreeToUpdate = resetCategoriesTreePriority(
|
||||||
|
@ -106,14 +106,14 @@ const onEditingModalClose = () => {
|
||||||
>
|
>
|
||||||
<div class="flex w-full flex-1 sm:w-auto">
|
<div class="flex w-full flex-1 sm:w-auto">
|
||||||
<span class="text-base font-medium">
|
<span class="text-base font-medium">
|
||||||
{{ categories.length }} 个分类
|
{{ categories?.length || 0 }} 个分类
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!categories.length" appear name="fade">
|
<Transition v-else-if="!categories?.length" appear name="fade">
|
||||||
<VEmpty message="你可以尝试刷新或者新建分类" title="当前没有分类">
|
<VEmpty message="你可以尝试刷新或者新建分类" title="当前没有分类">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
|
|
@ -1,67 +1,48 @@
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import type { Category } from "@halo-dev/api-client";
|
import type { Category } from "@halo-dev/api-client";
|
||||||
import { onUnmounted, type Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { onMounted, ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
|
import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
|
||||||
import { buildCategoriesTree } from "@/modules/contents/posts/categories/utils";
|
import { buildCategoriesTree } from "@/modules/contents/posts/categories/utils";
|
||||||
import { Dialog, Toast } from "@halo-dev/components";
|
import { Dialog, Toast } from "@halo-dev/components";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
interface usePostCategoryReturn {
|
interface usePostCategoryReturn {
|
||||||
categories: Ref<Category[]>;
|
categories: Ref<Category[] | undefined>;
|
||||||
categoriesTree: Ref<CategoryTree[]>;
|
categoriesTree: Ref<CategoryTree[]>;
|
||||||
loading: Ref<boolean>;
|
isLoading: Ref<boolean>;
|
||||||
handleFetchCategories: (fetchOptions?: { mute?: boolean }) => void;
|
handleFetchCategories: () => void;
|
||||||
handleDelete: (category: CategoryTree) => void;
|
handleDelete: (category: CategoryTree) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePostCategory(options?: {
|
export function usePostCategory(): usePostCategoryReturn {
|
||||||
fetchOnMounted: boolean;
|
|
||||||
}): usePostCategoryReturn {
|
|
||||||
const { fetchOnMounted } = options || {};
|
|
||||||
|
|
||||||
const categories = ref<Category[]>([] as Category[]);
|
|
||||||
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
|
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
|
||||||
const loading = ref(false);
|
|
||||||
const refreshInterval = ref();
|
|
||||||
|
|
||||||
const handleFetchCategories = async (fetchOptions?: { mute?: boolean }) => {
|
const {
|
||||||
try {
|
data: categories,
|
||||||
clearInterval(refreshInterval.value);
|
isLoading,
|
||||||
|
refetch,
|
||||||
if (!fetchOptions?.mute) {
|
} = useQuery({
|
||||||
loading.value = true;
|
queryKey: ["post-categories"],
|
||||||
}
|
queryFn: async () => {
|
||||||
const { data } =
|
const { data } =
|
||||||
await apiClient.extension.category.listcontentHaloRunV1alpha1Category({
|
await apiClient.extension.category.listcontentHaloRunV1alpha1Category({
|
||||||
page: 0,
|
page: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
});
|
});
|
||||||
categories.value = data.items;
|
|
||||||
categoriesTree.value = buildCategoriesTree(data.items);
|
|
||||||
|
|
||||||
const deletedCategories = categories.value.filter(
|
return data.items;
|
||||||
|
},
|
||||||
|
refetchInterval(data) {
|
||||||
|
const deletingCategories = data?.filter(
|
||||||
(category) => !!category.metadata.deletionTimestamp
|
(category) => !!category.metadata.deletionTimestamp
|
||||||
);
|
);
|
||||||
|
return deletingCategories?.length ? 3000 : false;
|
||||||
if (deletedCategories.length) {
|
},
|
||||||
refreshInterval.value = setInterval(() => {
|
refetchOnWindowFocus: false,
|
||||||
handleFetchCategories({ mute: true });
|
onSuccess(data) {
|
||||||
}, 3000);
|
categoriesTree.value = buildCategoriesTree(data);
|
||||||
}
|
},
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch categories", e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = async (category: CategoryTree) => {
|
const handleDelete = async (category: CategoryTree) => {
|
||||||
|
@ -81,21 +62,17 @@ export function usePostCategory(options?: {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to delete tag", e);
|
console.error("Failed to delete tag", e);
|
||||||
} finally {
|
} finally {
|
||||||
await handleFetchCategories();
|
await refetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchOnMounted && handleFetchCategories();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
categoriesTree,
|
categoriesTree,
|
||||||
loading,
|
isLoading,
|
||||||
handleFetchCategories,
|
handleFetchCategories: refetch,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ export const getCategoryPath = (
|
||||||
categories: CategoryTree[],
|
categories: CategoryTree[],
|
||||||
name: string,
|
name: string,
|
||||||
path: CategoryTree[] = []
|
path: CategoryTree[] = []
|
||||||
) => {
|
): CategoryTree[] | undefined => {
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
if (category.metadata && category.metadata.name === name) {
|
if (category.metadata && category.metadata.name === name) {
|
||||||
return path.concat([category]);
|
return path.concat([category]);
|
||||||
|
|
|
@ -46,9 +46,7 @@ const viewTypes = [
|
||||||
|
|
||||||
const viewType = ref("list");
|
const viewType = ref("list");
|
||||||
|
|
||||||
const { tags, loading, handleFetchTags, handleDelete } = usePostTag({
|
const { tags, isLoading, handleFetchTags, handleDelete } = usePostTag();
|
||||||
fetchOnMounted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const editingModal = ref(false);
|
const editingModal = ref(false);
|
||||||
const selectedTag = ref<Tag | null>(null);
|
const selectedTag = ref<Tag | null>(null);
|
||||||
|
@ -59,6 +57,8 @@ const handleOpenEditingModal = (tag: Tag | null) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectPrevious = () => {
|
const handleSelectPrevious = () => {
|
||||||
|
if (!tags.value) return;
|
||||||
|
|
||||||
const currentIndex = tags.value.findIndex(
|
const currentIndex = tags.value.findIndex(
|
||||||
(tag) => tag.metadata.name === selectedTag.value?.metadata.name
|
(tag) => tag.metadata.name === selectedTag.value?.metadata.name
|
||||||
);
|
);
|
||||||
|
@ -74,6 +74,8 @@ const handleSelectPrevious = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectNext = () => {
|
const handleSelectNext = () => {
|
||||||
|
if (!tags.value) return;
|
||||||
|
|
||||||
if (!selectedTag.value) {
|
if (!selectedTag.value) {
|
||||||
selectedTag.value = tags.value[0];
|
selectedTag.value = tags.value[0];
|
||||||
return;
|
return;
|
||||||
|
@ -140,7 +142,7 @@ onMounted(async () => {
|
||||||
>
|
>
|
||||||
<div class="flex w-full flex-1 sm:w-auto">
|
<div class="flex w-full flex-1 sm:w-auto">
|
||||||
<span class="text-base font-medium">
|
<span class="text-base font-medium">
|
||||||
{{ tags.length }} 个标签
|
{{ tags?.length || 0 }} 个标签
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
|
@ -159,8 +161,8 @@ onMounted(async () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<VLoading v-if="loading" />
|
<VLoading v-if="isLoading" />
|
||||||
<Transition v-else-if="!tags.length" appear name="fade">
|
<Transition v-else-if="!tags?.length" appear name="fade">
|
||||||
<VEmpty message="你可以尝试刷新或者新建标签" title="当前没有标签">
|
<VEmpty message="你可以尝试刷新或者新建标签" title="当前没有标签">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
|
|
@ -1,63 +1,39 @@
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import type { Tag } from "@halo-dev/api-client";
|
import type { Tag } from "@halo-dev/api-client";
|
||||||
import { onUnmounted, type Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import { Dialog, Toast } from "@halo-dev/components";
|
import { Dialog, Toast } from "@halo-dev/components";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
interface usePostTagReturn {
|
interface usePostTagReturn {
|
||||||
tags: Ref<Tag[]>;
|
tags: Ref<Tag[] | undefined>;
|
||||||
loading: Ref<boolean>;
|
isLoading: Ref<boolean>;
|
||||||
handleFetchTags: (fetchOptions?: { mute?: boolean }) => void;
|
handleFetchTags: () => void;
|
||||||
handleDelete: (tag: Tag) => void;
|
handleDelete: (tag: Tag) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePostTag(options?: {
|
export function usePostTag(): usePostTagReturn {
|
||||||
fetchOnMounted: boolean;
|
const {
|
||||||
}): usePostTagReturn {
|
data: tags,
|
||||||
const { fetchOnMounted } = options || {};
|
isLoading,
|
||||||
|
refetch,
|
||||||
const tags = ref<Tag[]>([] as Tag[]);
|
} = useQuery({
|
||||||
const loading = ref(false);
|
queryKey: ["post-tags"],
|
||||||
const refreshInterval = ref();
|
queryFn: async () => {
|
||||||
|
|
||||||
const handleFetchTags = async (fetchOptions?: { mute?: boolean }) => {
|
|
||||||
try {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
|
|
||||||
if (!fetchOptions?.mute) {
|
|
||||||
loading.value = true;
|
|
||||||
}
|
|
||||||
const { data } =
|
const { data } =
|
||||||
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
|
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
|
||||||
page: 0,
|
page: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
tags.value = data.items;
|
return data.items;
|
||||||
|
},
|
||||||
const deletedTags = tags.value.filter(
|
refetchInterval(data) {
|
||||||
|
const deletingTags = data?.filter(
|
||||||
(tag) => !!tag.metadata.deletionTimestamp
|
(tag) => !!tag.metadata.deletionTimestamp
|
||||||
);
|
);
|
||||||
|
return deletingTags?.length ? 3000 : false;
|
||||||
if (deletedTags.length) {
|
},
|
||||||
refreshInterval.value = setInterval(() => {
|
refetchOnWindowFocus: false,
|
||||||
handleFetchTags({ mute: true });
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to fetch tags", e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
|
||||||
clearInterval(refreshInterval.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = async (tag: Tag) => {
|
const handleDelete = async (tag: Tag) => {
|
||||||
|
@ -75,20 +51,16 @@ export function usePostTag(options?: {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to delete tag", e);
|
console.error("Failed to delete tag", e);
|
||||||
} finally {
|
} finally {
|
||||||
await handleFetchTags();
|
await refetch();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchOnMounted && handleFetchTags();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tags,
|
tags,
|
||||||
loading,
|
isLoading,
|
||||||
handleFetchTags,
|
handleFetchTags: refetch,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,15 @@ import {
|
||||||
VEntityField,
|
VEntityField,
|
||||||
IconExternalLinkLine,
|
IconExternalLinkLine,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import type { ListedPost } from "@halo-dev/api-client";
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
|
||||||
const posts = ref<ListedPost[]>([] as ListedPost[]);
|
const { data } = useQuery<ListedPost[]>({
|
||||||
|
queryKey: ["widget-recent-posts"],
|
||||||
const handleFetchPosts = async () => {
|
queryFn: async () => {
|
||||||
try {
|
|
||||||
const { data } = await apiClient.post.listPosts({
|
const { data } = await apiClient.post.listPosts({
|
||||||
labelSelector: [
|
labelSelector: [
|
||||||
`${postLabels.DELETED}=false`,
|
`${postLabels.DELETED}=false`,
|
||||||
|
@ -26,13 +25,10 @@ const handleFetchPosts = async () => {
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
});
|
});
|
||||||
posts.value = data.items;
|
return data.items;
|
||||||
} catch (e) {
|
},
|
||||||
console.error("Failed to fetch posts", e);
|
refetchOnWindowFocus: false,
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(handleFetchPosts);
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VCard
|
<VCard
|
||||||
|
@ -41,7 +37,7 @@ onMounted(handleFetchPosts);
|
||||||
title="最近文章"
|
title="最近文章"
|
||||||
>
|
>
|
||||||
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
<ul class="box-border h-full w-full divide-y divide-gray-100" role="list">
|
||||||
<li v-for="(post, index) in posts" :key="index">
|
<li v-for="(post, index) in data" :key="index">
|
||||||
<VEntity>
|
<VEntity>
|
||||||
<template #start>
|
<template #start>
|
||||||
<VEntityField
|
<VEntityField
|
||||||
|
|
Loading…
Reference in New Issue