refactor: logic of attachment data filtering (#4194)

#### What type of PR is this?

/area console
/kind improvement
/milestone 2.8.x

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

重构附件数据管理的筛选条件逻辑以及 UI。

<img width="1645" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/6b13045e-ab6b-4306-a1bb-f19d97115191">


Ref https://github.com/halo-dev/halo/pull/4182
Ref https://github.com/halo-dev/halo/issues/4181

#### Special notes for your reviewer:

需要测试:

1. 测试附件的筛选条件包括关键词筛选功能是否正常。

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

```release-note
重构 Console 端附件数据列表的筛选项 UI 和逻辑。
```
pull/4212/head
Ryan Wang 2023-07-13 11:31:17 +08:00 committed by GitHub
parent 6bba5073fa
commit efcf526f1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 81 additions and 179 deletions

View File

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconCheckboxFill, IconCheckboxFill,
@ -30,7 +29,7 @@ import AttachmentUploadModal from "./components/AttachmentUploadModal.vue";
import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue"; import AttachmentPoliciesModal from "./components/AttachmentPoliciesModal.vue";
import AttachmentGroupList from "./components/AttachmentGroupList.vue"; import AttachmentGroupList from "./components/AttachmentGroupList.vue";
import { computed, onMounted, ref, watch } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import type { Attachment, Group, Policy } from "@halo-dev/api-client"; import type { Attachment, Group } from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy"; import { useFetchAttachmentPolicy } from "./composables/use-attachment-policy";
@ -42,8 +41,6 @@ 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 { getNode } from "@formkit/core";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue"; import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
@ -60,81 +57,35 @@ const { groups, handleFetchGroups } = useFetchAttachmentGroup();
const selectedGroup = ref<Group>(); const selectedGroup = ref<Group>();
// Filter // Filter
interface SortItem { const keyword = ref<string>("");
label: string; const page = ref<number>(1);
value: string; const size = ref<number>(60);
} const selectedPolicy = ref();
const SortItems: SortItem[] = [
{
label: t("core.attachment.filters.sort.items.create_time_desc"),
value: "creationTimestamp,desc",
},
{
label: t("core.attachment.filters.sort.items.create_time_asc"),
value: "creationTimestamp,asc",
},
{
label: t("core.attachment.filters.sort.items.size_desc"),
value: "size,desc",
},
{
label: t("core.attachment.filters.sort.items.size_asc"),
value: "size,asc",
},
];
const selectedPolicy = ref<Policy>();
const selectedUser = ref(); const selectedUser = ref();
const selectedSortItem = ref<SortItem>(); const selectedSort = ref();
const selectedSortItemValue = computed(() => {
return selectedSortItem.value?.value;
});
function handleSelectPolicy(policy: Policy | undefined) { watch(
selectedPolicy.value = policy; () => [
page.value = 1; selectedPolicy.value,
} selectedUser.value,
selectedSort.value,
function handleSortItemChange(sortItem?: SortItem) { keyword.value,
selectedSortItem.value = sortItem; ],
page.value = 1; () => {
} page.value = 1;
function handleKeywordChange() {
const keywordNode = getNode("keywordInput");
if (keywordNode) {
keyword.value = keywordNode._value as string;
} }
page.value = 1; );
}
function handleClearKeyword() {
keyword.value = "";
page.value = 1;
}
const hasFilters = computed(() => { const hasFilters = computed(() => {
return ( return selectedPolicy.value || selectedUser.value || selectedSort.value;
selectedPolicy.value ||
selectedUser.value ||
selectedSortItem.value ||
keyword.value
);
}); });
function handleClearFilters() { function handleClearFilters() {
selectedPolicy.value = undefined; selectedPolicy.value = undefined;
selectedUser.value = undefined; selectedUser.value = undefined;
selectedSortItem.value = undefined; selectedSort.value = undefined;
keyword.value = "";
page.value = 1;
} }
const keyword = ref<string>("");
const page = ref<number>(1);
const size = ref<number>(60);
const { const {
attachments, attachments,
selectedAttachment, selectedAttachment,
@ -154,10 +105,14 @@ const {
handleReset, handleReset,
} = useAttachmentControl({ } = useAttachmentControl({
group: selectedGroup, group: selectedGroup,
policy: selectedPolicy, policy: computed(() => {
return policies.value?.find(
(policy) => policy.metadata.name === selectedPolicy.value
);
}),
user: selectedUser, user: selectedUser,
keyword: keyword, keyword: keyword,
sort: selectedSortItemValue, sort: selectedSort,
page: page, page: page,
size: size, size: size,
}); });
@ -348,55 +303,10 @@ onMounted(() => {
/> />
</div> </div>
<div class="flex w-full flex-1 items-center sm:w-auto"> <div class="flex w-full flex-1 items-center sm:w-auto">
<div <SearchInput
v-if="!selectedAttachments.size" v-if="!selectedAttachments.size"
class="flex items-center gap-2" v-model="keyword"
> />
<FormKit
id="keywordInput"
outer-class="!p-0"
:placeholder="$t('core.common.placeholder.search')"
type="text"
name="keyword"
:model-value="keyword"
@keyup.enter="handleKeywordChange"
></FormKit>
<FilterTag v-if="keyword" @close="handleClearKeyword()">
{{
$t("core.common.filters.results.keyword", {
keyword: keyword,
})
}}
</FilterTag>
<FilterTag
v-if="selectedPolicy"
@close="handleSelectPolicy(undefined)"
>
{{
$t("core.attachment.filters.storage_policy.result", {
storage_policy: selectedPolicy.spec.displayName,
})
}}
</FilterTag>
<FilterTag
v-if="selectedSortItem"
@click="handleSortItemChange()"
>
{{
$t("core.common.filters.results.sort", {
sort: selectedSortItem.label,
})
}}
</FilterTag>
<FilterCleanButton
v-if="hasFilters"
@click="handleClearFilters"
/>
</div>
<VSpace v-else> <VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch"> <VButton type="danger" @click="handleDeleteInBatch">
{{ $t("core.common.buttons.delete") }} {{ $t("core.common.buttons.delete") }}
@ -424,72 +334,64 @@ onMounted(() => {
</div> </div>
<div class="mt-4 flex sm:mt-0"> <div class="mt-4 flex sm:mt-0">
<VSpace spacing="lg"> <VSpace spacing="lg">
<VDropdown> <FilterCleanButton
<div v-if="hasFilters"
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" @click="handleClearFilters"
> />
<span class="mr-0.5"> <FilterDropdown
{{ v-model="selectedPolicy"
$t("core.attachment.filters.storage_policy.label") :label="
}} $t('core.attachment.filters.storage_policy.label')
</span> "
<span> :items="[
<IconArrowDown /> {
</span> label: t('core.common.filters.item_labels.all'),
</div> },
<template #popper> ...(policies?.map((policy) => {
<VDropdownItem return {
v-for="(policy, index) in policies" label: policy.spec.displayName,
:key="index" value: policy.metadata.name,
:selected="policy === selectedPolicy" };
@click="handleSelectPolicy(policy)" }) || []),
> ]"
{{ policy.spec.displayName }} />
</VDropdownItem>
</template>
</VDropdown>
<UserFilterDropdown <UserFilterDropdown
v-model="selectedUser" v-model="selectedUser"
:label="$t('core.attachment.filters.owner.label')" :label="$t('core.attachment.filters.owner.label')"
/> />
<!-- TODO: add filter by ref support --> <FilterDropdown
<VDropdown v-if="false"> v-model="selectedSort"
<div :label="$t('core.common.filters.labels.sort')"
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" :items="[
> {
<span class="mr-0.5">引用位置</span> label: t('core.common.filters.item_labels.default'),
<span> },
<IconArrowDown /> {
</span> label: t(
</div> 'core.attachment.filters.sort.items.create_time_desc'
<template #popper> ),
<VDropdownItem> 未被引用 </VDropdownItem> value: 'creationTimestamp,desc',
<VDropdownItem> 文章 </VDropdownItem> },
</template> {
</VDropdown> label: t(
<VDropdown> 'core.attachment.filters.sort.items.create_time_asc'
<div ),
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black" value: 'creationTimestamp,asc',
> },
<span class="mr-0.5"> {
{{ $t("core.common.filters.labels.sort") }} label: t(
</span> 'core.attachment.filters.sort.items.size_desc'
<span> ),
<IconArrowDown /> value: 'size,desc',
</span> },
</div> {
<template #popper> label: t(
<VDropdownItem 'core.attachment.filters.sort.items.size_asc'
v-for="(sortItem, index) in SortItems" ),
:key="index" value: 'size,asc',
:selected="sortItem.value === selectedSortItem?.value" },
@click="handleSortItemChange(sortItem)" ]"
> />
{{ sortItem.label }}
</VDropdownItem>
</template>
</VDropdown>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div <div
v-for="(item, index) in viewTypes" v-for="(item, index) in viewTypes"