perf: improve the list filter style and support clear all (#702)

#### What type of PR is this?

/kind improvement
/milestone 2.0

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

优化 Console 数据列表的筛选标签样式,以及支持清空所有筛选条件。

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

Fixes https://github.com/halo-dev/halo/issues/2657

#### Screenshots:

<img width="1663" alt="image" src="https://user-images.githubusercontent.com/21301288/203353043-b5e7631f-cc02-4368-b770-42b53e1dbf78.png">

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

测试方式:

1. 测试文章、自定义页面、附件、插件、评论管理中的筛选功能。

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

```release-note
优化 Console 数据列表的筛选标签样式,以及支持清空所有筛选条件。
```
pull/699/head^2
Ryan Wang 2022-11-23 12:59:28 +08:00 committed by GitHub
parent 2350f541d7
commit b79eccb6a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 483 additions and 261 deletions

View File

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { IconDeleteBin } from "@halo-dev/components";
</script>
<template>
<div
class="group inline-flex cursor-pointer items-center justify-center rounded-full bg-primary px-1 py-1 opacity-80 ring-1 ring-primary transition-all duration-300 hover:opacity-100 hover:shadow-sm hover:ring-opacity-50"
>
<div class="h-4 w-4 rounded-full transition-all">
<IconDeleteBin
class="h-full w-full text-gray-200 transition-all group-hover:text-white"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,25 @@
<script lang="ts" setup>
import { IconCloseCircle } from "@halo-dev/components";
const emit = defineEmits<{
(event: "close"): void;
}>();
</script>
<template>
<div
class="group inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-primary px-2 py-1 ring-1 ring-primary transition-all duration-300 hover:shadow-sm hover:ring-opacity-50"
>
<span v-if="$slots.default" class="text-xs text-white transition-all">
<slot />
</span>
<div
class="h-4 w-4 rounded-full ring-white transition-all group-hover:ring-1"
@click="emit('close')"
>
<IconCloseCircle
class="h-4 w-4 text-gray-200 transition-all group-hover:text-white"
/>
</div>
</div>
</template>

View File

@ -15,7 +15,6 @@ import {
VPagination,
VSpace,
VEmpty,
IconCloseCircle,
IconFolder,
VStatusDot,
VEntity,
@ -40,6 +39,9 @@ import { isImage } from "@/utils/image";
import { useRouteQuery } from "@vueuse/router";
import { useFetchAttachmentGroup } from "./composables/use-attachment-group";
import { usePermission } from "@/utils/permission";
import FilterTag from "@/components/filter/FilterTag.vue";
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core";
const { currentUserHasPermission } = usePermission();
@ -90,17 +92,47 @@ const selectedSortItemValue = computed(() => {
function handleSelectPolicy(policy: Policy | undefined) {
selectedPolicy.value = policy;
handleFetchAttachments();
handleFetchAttachments(1);
}
function handleSelectUser(user: User | undefined) {
selectedUser.value = user;
handleFetchAttachments();
handleFetchAttachments(1);
}
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
handleFetchAttachments();
handleFetchAttachments(1);
}
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
handleFetchAttachments(1);
}
function handleClearKeyword() {
keyword.value = "";
handleFetchAttachments(1);
}
const hasFilters = computed(() => {
return (
selectedPolicy.value ||
selectedUser.value ||
selectedSortItem.value ||
keyword.value
);
});
function handleClearFilters() {
selectedPolicy.value = undefined;
selectedUser.value = undefined;
selectedSortItem.value = undefined;
keyword.value = "";
handleFetchAttachments(1);
}
const {
@ -322,57 +354,44 @@ onMounted(() => {
class="flex items-center gap-2"
>
<FormKit
v-model="keyword"
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
@keyup.enter="handleFetchAttachments()"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<div
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
<FilterTag
v-if="selectedPolicy"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleSelectPolicy(undefined)"
>
<span
class="text-xs text-gray-600 group-hover:text-gray-900"
>
存储策略{{ selectedPolicy?.spec.displayName }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSelectPolicy(undefined)"
/>
</div>
存储策略{{ selectedPolicy?.spec.displayName }}
</FilterTag>
<div
<FilterTag
v-if="selectedUser"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleSelectUser(undefined)"
>
<span
class="text-xs text-gray-600 group-hover:text-gray-900"
>
上传者{{ selectedUser?.spec.displayName }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSelectUser(undefined)"
/>
</div>
上传者{{ selectedUser?.spec.displayName }}
</FilterTag>
<div
<FilterTag
v-if="selectedSortItem"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@click="handleSortItemChange()"
>
<span
class="text-xs text-gray-600 group-hover:text-gray-900"
>
排序{{ selectedSortItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSortItemChange()"
/>
</div>
排序{{ selectedSortItem.label }}
</FilterTag>
<FilteCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch">
@ -521,7 +540,7 @@ onMounted(() => {
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchAttachments"
@click="handleFetchAttachments()"
>
<IconRefreshLine
:class="{ 'animate-spin text-gray-900': loading }"

View File

@ -19,7 +19,7 @@ interface useAttachmentControlReturn {
selectedAttachment: Ref<Attachment | undefined>;
selectedAttachments: Ref<Set<Attachment>>;
checkedAll: Ref<boolean>;
handleFetchAttachments: () => void;
handleFetchAttachments: (page?: number) => void;
handlePaginationChange: ({
page,
size,
@ -68,11 +68,16 @@ export function useAttachmentControl(filterOptions?: {
const checkedAll = ref(false);
const refreshInterval = ref();
const handleFetchAttachments = async () => {
const handleFetchAttachments = async (page?: number) => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
if (page) {
attachments.value.page = page;
}
const { data } = await apiClient.attachment.searchAttachments({
policy: policy?.value?.metadata.name,
displayName: keyword?.value,

View File

@ -7,7 +7,6 @@ import {
VPageHeader,
VPagination,
VSpace,
IconCloseCircle,
IconRefreshLine,
VEmpty,
Dialog,
@ -19,9 +18,12 @@ import type {
ListedCommentList,
User,
} from "@halo-dev/api-client";
import { onMounted, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import { onBeforeRouteLeave } from "vue-router";
import FilterTag from "@/components/filter/FilterTag.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core";
const comments = ref<ListedCommentList>({
page: 1,
@ -41,11 +43,16 @@ const selectedCommentNames = ref<string[]>([]);
const keyword = ref("");
const refreshInterval = ref();
const handleFetchComments = async () => {
const handleFetchComments = async (page?: number) => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
if (page) {
comments.value.page = page;
}
const { data } = await apiClient.comment.listComments({
page: comments.value.page,
size: comments.value.size,
@ -212,13 +219,16 @@ const SortFilterItems: {
value: "CREATE_TIME",
},
];
const selectedApprovedFilterItem = ref<{ label: string; value?: boolean }>(
ApprovedFilterItems[0]
);
const selectedSortFilterItem = ref<{
label: string;
value?: Sort;
}>(SortFilterItems[0]);
const selectedUser = ref<User>();
const handleApprovedFilterItemChange = (filterItem: {
@ -227,7 +237,7 @@ const handleApprovedFilterItemChange = (filterItem: {
}) => {
selectedApprovedFilterItem.value = filterItem;
selectedCommentNames.value = [];
handlePaginationChange({ page: 1, size: 20 });
handleFetchComments(1);
};
const handleSortFilterItemChange = (filterItem: {
@ -236,12 +246,42 @@ const handleSortFilterItemChange = (filterItem: {
}) => {
selectedSortFilterItem.value = filterItem;
selectedCommentNames.value = [];
handlePaginationChange({ page: 1, size: 20 });
handleFetchComments(1);
};
function handleSelectUser(user: User | undefined) {
selectedUser.value = user;
handlePaginationChange({ page: 1, size: 20 });
handleFetchComments(1);
}
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
handleFetchComments(1);
}
function handleClearKeyword() {
keyword.value = "";
handleFetchComments(1);
}
const hasFilters = computed(() => {
return (
selectedApprovedFilterItem.value.value !== undefined ||
selectedSortFilterItem.value.value !== "LAST_REPLY_TIME" ||
selectedUser.value ||
keyword.value
);
});
function handleClearFilters() {
selectedApprovedFilterItem.value = ApprovedFilterItems[0];
selectedSortFilterItem.value = SortFilterItems[0];
selectedUser.value = undefined;
keyword.value = "";
handleFetchComments(1);
}
</script>
<template>
@ -275,49 +315,46 @@ function handleSelectUser(user: User | undefined) {
class="flex items-center gap-2"
>
<FormKit
v-model="keyword"
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
@keyup.enter="handleFetchComments"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<div
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
<FilterTag
v-if="selectedApprovedFilterItem.value != undefined"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="
handleApprovedFilterItemChange(ApprovedFilterItems[0])
"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
状态{{ selectedApprovedFilterItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="
handleApprovedFilterItemChange(ApprovedFilterItems[0])
"
/>
</div>
<div
状态{{ selectedApprovedFilterItem.label }}
</FilterTag>
<FilterTag
v-if="selectedUser"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleSelectUser(undefined)"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
评论者{{ selectedUser?.spec.displayName }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSelectUser(undefined)"
/>
</div>
<div
评论者{{ selectedUser?.spec.displayName }}
</FilterTag>
<FilterTag
v-if="selectedSortFilterItem.value != 'LAST_REPLY_TIME'"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleSortFilterItemChange(SortFilterItems[0])"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
排序{{ selectedSortFilterItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSortFilterItemChange(SortFilterItems[0])"
/>
</div>
排序{{ selectedSortFilterItem.label }}
</FilterTag>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else>
<VButton type="secondary" @click="handleApproveInBatch">
@ -409,7 +446,7 @@ function handleSelectUser(user: User | undefined) {
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchComments"
@click="handleFetchComments()"
>
<IconRefreshLine
:class="{ 'animate-spin text-gray-900': loading }"

View File

@ -290,12 +290,6 @@ const subjectRefResult = computed(() => {
class="w-28 min-w-[7rem]"
:title="comment?.owner?.displayName"
:description="comment?.owner?.email"
:route="{
name: 'UserDetail',
params: {
name: comment?.owner?.name,
},
}"
></VEntityField>
<VEntityField>
<template #description>

View File

@ -22,6 +22,8 @@ import { formatDatetime } from "@/utils/date";
import { onBeforeRouteLeave, RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
const { currentUserHasPermission } = usePermission();
@ -42,12 +44,16 @@ const checkedAll = ref(false);
const refreshInterval = ref();
const keyword = ref("");
const handleFetchSinglePages = async () => {
const handleFetchSinglePages = async (page?: number) => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
if (page) {
singlePages.value.page = page;
}
const { data } = await apiClient.singlePage.listSinglePages({
labelSelector: [`content.halo.run/deleted=true`],
page: singlePages.value.page,
@ -198,6 +204,20 @@ watch(selectedPageNames, (newValue) => {
});
onMounted(handleFetchSinglePages);
// Filters
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
handleFetchSinglePages(1);
}
function handleClearKeyword() {
keyword.value = "";
handleFetchSinglePages(1);
}
</script>
<template>
@ -245,11 +265,18 @@ onMounted(handleFetchSinglePages);
class="flex items-center gap-2"
>
<FormKit
v-model="keyword"
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
@keyup.enter="handleFetchSinglePages"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
</div>
<VSpace v-else>
<VButton type="danger" @click="handleDeletePermanentlyInBatch">
@ -265,7 +292,7 @@ onMounted(handleFetchSinglePages);
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchSinglePages"
@click="handleFetchSinglePages()"
>
<IconRefreshLine
:class="{ 'animate-spin text-gray-900': loading }"

View File

@ -6,7 +6,6 @@ import {
IconEye,
IconEyeOff,
IconTeam,
IconCloseCircle,
IconAddCircle,
IconRefreshLine,
VButton,
@ -22,7 +21,7 @@ import {
} from "@halo-dev/components";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import { onMounted, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import type {
ListedSinglePageList,
SinglePage,
@ -34,6 +33,9 @@ import { onBeforeRouteLeave, RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
import { singlePageLabels } from "@/constants/labels";
import FilterTag from "@/components/filter/FilterTag.vue";
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core";
const { currentUserHasPermission } = usePermission();
@ -55,7 +57,7 @@ const selectedPageNames = ref<string[]>([]);
const checkedAll = ref(false);
const refreshInterval = ref();
const handleFetchSinglePages = async () => {
const handleFetchSinglePages = async (page?: number) => {
try {
clearInterval(refreshInterval.value);
@ -74,6 +76,10 @@ const handleFetchSinglePages = async () => {
);
}
if (page) {
singlePages.value.page = page;
}
const { data } = await apiClient.singlePage.listSinglePages({
labelSelector,
page: singlePages.value.page,
@ -354,22 +360,54 @@ const keyword = ref("");
function handleVisibleItemChange(visibleItem: VisibleItem) {
selectedVisibleItem.value = visibleItem;
handleFetchSinglePages();
handleFetchSinglePages(1);
}
const handleSelectUser = (user?: User) => {
selectedContributor.value = user;
handleFetchSinglePages();
handleFetchSinglePages(1);
};
function handlePublishStatusItemChange(publishStatusItem: PublishStatusItem) {
selectedPublishStatusItem.value = publishStatusItem;
handleFetchSinglePages();
handleFetchSinglePages(1);
}
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
handleFetchSinglePages();
handleFetchSinglePages(1);
}
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
handleFetchSinglePages(1);
}
function handleClearKeyword() {
keyword.value = "";
handleFetchSinglePages(1);
}
const hasFilters = computed(() => {
return (
selectedContributor.value ||
selectedVisibleItem.value.value ||
selectedPublishStatusItem.value.value !== undefined ||
selectedSortItem.value ||
keyword.value
);
});
function handleClearFilters() {
selectedContributor.value = undefined;
selectedVisibleItem.value = VisibleItems[0];
selectedPublishStatusItem.value = PublishStatusItems[0];
selectedSortItem.value = undefined;
keyword.value = "";
handleFetchSinglePages(1);
}
</script>
@ -411,60 +449,48 @@ function handleSortItemChange(sortItem?: SortItem) {
class="flex items-center gap-2"
>
<FormKit
v-model="keyword"
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
@keyup.enter="handleFetchSinglePages"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<div
v-if="selectedPublishStatusItem.value"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
<FilterTag
v-if="selectedPublishStatusItem.value !== undefined"
@close="handlePublishStatusItemChange(PublishStatusItems[0])"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
状态{{ selectedPublishStatusItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handlePublishStatusItemChange(PublishStatusItems[0])"
/>
</div>
<div
状态{{ selectedPublishStatusItem.label }}
</FilterTag>
<FilterTag
v-if="selectedVisibleItem.value"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleVisibleItemChange(VisibleItems[0])"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
可见性{{ selectedVisibleItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleVisibleItemChange(VisibleItems[0])"
/>
</div>
<div
v-if="selectedContributor"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
作者{{ selectedContributor?.spec.displayName }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSelectUser(undefined)"
/>
</div>
<div
可见性{{ selectedVisibleItem.label }}
</FilterTag>
<FilterTag v-if="selectedContributor" @close="handleSelectUser()">
作者{{ selectedContributor?.spec.displayName }}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleSortItemChange()"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
排序{{ selectedSortItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSortItemChange()"
/>
</div>
排序{{ selectedSortItem.label }}
</FilterTag>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch"></VButton>
@ -574,7 +600,7 @@ function handleSortItemChange(sortItem?: SortItem) {
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchSinglePages"
@click="handleFetchSinglePages()"
>
<IconRefreshLine
:class="{ 'animate-spin text-gray-900': loading }"

View File

@ -23,6 +23,8 @@ import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { getNode } from "@formkit/core";
import FilterTag from "@/components/filter/FilterTag.vue";
const { currentUserHasPermission } = usePermission();
@ -43,12 +45,16 @@ const selectedPostNames = ref<string[]>([]);
const refreshInterval = ref();
const keyword = ref("");
const handleFetchPosts = async () => {
const handleFetchPosts = async (page?: number) => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
if (page) {
posts.value.page = page;
}
const { data } = await apiClient.post.listPosts({
labelSelector: [`content.halo.run/deleted=true`],
page: posts.value.page,
@ -191,6 +197,19 @@ watch(selectedPostNames, (newValue) => {
onMounted(() => {
handleFetchPosts();
});
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
handleFetchPosts(1);
}
function handleClearKeyword() {
keyword.value = "";
handleFetchPosts(1);
}
</script>
<template>
<VPageHeader title="文章回收站">
@ -238,12 +257,18 @@ onMounted(() => {
class="flex items-center gap-2"
>
<FormKit
v-model="keyword"
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
@keyup.enter="handleFetchPosts"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
</div>
<VSpace v-else>
<VButton type="danger" @click="handleDeletePermanentlyInBatch">
@ -259,7 +284,7 @@ onMounted(() => {
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchPosts"
@click="handleFetchPosts()"
>
<IconRefreshLine
:class="{ 'animate-spin text-gray-900': loading }"

View File

@ -8,7 +8,6 @@ import {
IconEye,
IconEyeOff,
IconTeam,
IconCloseCircle,
IconRefreshLine,
Dialog,
VButton,
@ -25,7 +24,7 @@ import {
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import PostSettingModal from "./components/PostSettingModal.vue";
import PostTag from "../posts/tags/components/PostTag.vue";
import { onMounted, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import type {
User,
Category,
@ -40,6 +39,9 @@ import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-t
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
import { postLabels } from "@/constants/labels";
import FilterTag from "@/components/filter/FilterTag.vue";
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core";
const { currentUserHasPermission } = usePermission();
@ -61,7 +63,7 @@ const checkedAll = ref(false);
const selectedPostNames = ref<string[]>([]);
const refreshInterval = ref();
const handleFetchPosts = async () => {
const handleFetchPosts = async (page?: number) => {
try {
clearInterval(refreshInterval.value);
@ -93,6 +95,10 @@ const handleFetchPosts = async () => {
);
}
if (page) {
posts.value.page = page;
}
const { data } = await apiClient.post.listPosts({
labelSelector,
page: posts.value.page,
@ -372,33 +378,69 @@ const keyword = ref("");
function handleVisibleItemChange(visibleItem: VisibleItem) {
selectedVisibleItem.value = visibleItem;
handleFetchPosts();
handleFetchPosts(1);
}
function handlePublishStatusItemChange(publishStatusItem: PublishStatuItem) {
selectedPublishStatusItem.value = publishStatusItem;
handleFetchPosts();
handleFetchPosts(1);
}
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
handleFetchPosts();
handleFetchPosts(1);
}
function handleCategoryChange(category?: Category) {
selectedCategory.value = category;
handleFetchPosts();
handleFetchPosts(1);
}
function handleTagChange(tag?: Tag) {
selectedTag.value = tag;
handleFetchPosts();
handleFetchPosts(1);
}
function handleContributorChange(user?: User) {
selectedContributor.value = user;
handleFetchPosts();
handleFetchPosts(1);
}
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
handleFetchPosts(1);
}
function handleClearKeyword() {
keyword.value = "";
handleFetchPosts(1);
}
function handleClearFilters() {
selectedVisibleItem.value = VisibleItems[0];
selectedPublishStatusItem.value = PublishStatuItems[0];
selectedSortItem.value = undefined;
selectedCategory.value = undefined;
selectedTag.value = undefined;
selectedContributor.value = undefined;
keyword.value = "";
handleFetchPosts(1);
}
const hasFilters = computed(() => {
return (
selectedVisibleItem.value.value ||
selectedPublishStatusItem.value.value !== undefined ||
selectedSortItem.value ||
selectedCategory.value ||
selectedTag.value ||
selectedContributor.value ||
keyword.value
);
});
</script>
<template>
<PostSettingModal
@ -462,86 +504,62 @@ function handleContributorChange(user?: User) {
class="flex items-center gap-2"
>
<FormKit
v-model="keyword"
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
@keyup.enter="handleFetchPosts"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<div
v-if="selectedPublishStatusItem.value"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
状态{{ selectedPublishStatusItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handlePublishStatusItemChange(PublishStatuItems[0])"
/>
</div>
<div
v-if="selectedVisibleItem.value"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
可见性{{ selectedVisibleItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleVisibleItemChange(VisibleItems[0])"
/>
</div>
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
<div
<FilterTag
v-if="selectedPublishStatusItem.value !== undefined"
@close="handlePublishStatusItemChange(PublishStatuItems[0])"
>
状态{{ selectedPublishStatusItem.label }}
</FilterTag>
<FilterTag
v-if="selectedVisibleItem.value"
@close="handleVisibleItemChange(VisibleItems[0])"
>
可见性{{ selectedVisibleItem.label }}
</FilterTag>
<FilterTag
v-if="selectedCategory"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleCategoryChange()"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
分类{{ selectedCategory.spec.displayName }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleCategoryChange()"
/>
</div>
<div
v-if="selectedTag"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
标签{{ selectedTag.spec.displayName }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleTagChange()"
/>
</div>
<div
分类{{ selectedCategory.spec.displayName }}
</FilterTag>
<FilterTag v-if="selectedTag" @click="handleTagChange()">
标签{{ selectedTag.spec.displayName }}
</FilterTag>
<FilterTag
v-if="selectedContributor"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleContributorChange()"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
作者{{ selectedContributor.spec.displayName }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleContributorChange()"
/>
</div>
<div
作者{{ selectedContributor.spec.displayName }}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleSortItemChange()"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
排序{{ selectedSortItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSortItemChange()"
/>
</div>
排序{{ selectedSortItem.label }}
</FilterTag>
<FilteCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch">
@ -792,7 +810,7 @@ function handleContributorChange(user?: User) {
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchPosts"
@click="handleFetchPosts()"
>
<IconRefreshLine
:class="{ 'animate-spin text-gray-900': loading }"

View File

@ -2,7 +2,6 @@
import {
IconAddCircle,
IconArrowDown,
IconCloseCircle,
IconPlug,
IconRefreshLine,
VButton,
@ -14,11 +13,14 @@ import {
} from "@halo-dev/components";
import PluginListItem from "./components/PluginListItem.vue";
import PluginUploadModal from "./components/PluginUploadModal.vue";
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { apiClient } from "@/utils/api-client";
import type { PluginList } from "@halo-dev/api-client";
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
import FilterTag from "@/components/filter/FilterTag.vue";
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core";
const { currentUserHasPermission } = usePermission();
@ -38,12 +40,16 @@ const pluginInstall = ref(false);
const keyword = ref("");
const refreshInterval = ref();
const handleFetchPlugins = async () => {
const handleFetchPlugins = async (page?: number) => {
try {
clearInterval(refreshInterval.value);
loading.value = true;
if (page) {
plugins.value.page = page;
}
const { data } = await apiClient.plugin.listPlugins({
page: plugins.value.page,
size: plugins.value.size,
@ -132,12 +138,40 @@ const selectedSortItem = ref<SortItem>();
function handleEnabledItemChange(enabledItem: EnabledItem) {
selectedEnabledItem.value = enabledItem;
handleFetchPlugins();
handleFetchPlugins(1);
}
function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem;
handleFetchPlugins();
handleFetchPlugins(1);
}
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
}
handleFetchPlugins(1);
}
function handleClearKeyword() {
keyword.value = "";
handleFetchPlugins(1);
}
const hasFilters = computed(() => {
return (
selectedEnabledItem.value?.value !== undefined ||
selectedSortItem.value?.value ||
keyword.value
);
});
function handleClearFilters() {
selectedEnabledItem.value = undefined;
selectedSortItem.value = undefined;
keyword.value = "";
handleFetchPlugins(1);
}
</script>
<template>
@ -174,37 +208,34 @@ function handleSortItemChange(sortItem?: SortItem) {
>
<div class="flex w-full flex-1 items-center gap-2 sm:w-auto">
<FormKit
v-model="keyword"
id="keywordInput"
outer-class="!p-0"
placeholder="输入关键词搜索"
type="text"
@keyup.enter="
handlePaginationChange({ page: 1, size: plugins.size })
"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<div
<FilterTag v-if="keyword" @close="handleClearKeyword()">
关键词{{ keyword }}
</FilterTag>
<FilterTag
v-if="selectedEnabledItem?.value !== undefined"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleEnabledItemChange(EnabledItems[0])"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
启用状态{{ selectedEnabledItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleEnabledItemChange(EnabledItems[0])"
/>
</div>
<div
启用状态{{ selectedEnabledItem.label }}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
@close="handleSortItemChange()"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
排序{{ selectedSortItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handleSortItemChange()"
/>
</div>
排序{{ selectedSortItem.label }}
</FilterTag>
<FilteCleanButton v-if="hasFilters" @click="handleClearFilters" />
</div>
<div class="mt-4 flex sm:mt-0">
<VSpace spacing="lg">
@ -265,7 +296,7 @@ function handleSortItemChange(sortItem?: SortItem) {
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchPlugins"
@click="handleFetchPlugins()"
>
<IconRefreshLine
:class="{ 'animate-spin text-gray-900': loading }"