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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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