mirror of https://github.com/halo-dev/halo-admin
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
parent
2350f541d7
commit
b79eccb6a2
|
@ -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>
|
|
@ -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>
|
|
@ -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 }"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }"
|
||||||
|
|
|
@ -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 }"
|
||||||
|
|
|
@ -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 }"
|
||||||
|
|
|
@ -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 }"
|
||||||
|
|
|
@ -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 }"
|
||||||
|
|
Loading…
Reference in New Issue