mirror of https://github.com/halo-dev/halo
feat: record the post query conditions in the route query parameters (#4102)
#### What type of PR is this? /area console /kind feature /milestone 2.8.x #### What this PR does / why we need it: 在文章数据管理列表页面路由中记录查询条件,包括分页信息、筛选信息等。可以保证在刷新页面或者从文章编辑页面返回时保留之前的查询状态。 <img width="1758" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/270948d6-d585-4b36-ad3a-93064cf47548"> TODO: - [x] 记录筛选条件,因为路由参数只能使用基本类型,但是原来的筛选条件的变量都是完整对象。 #### Which issue(s) this PR fixes: Fixes #4098 #### Special notes for your reviewer: 需要测试: 1. 文章管理列表的所有筛选项是否可以正常工作。 2. 尝试设置部分筛选,然后刷新页面,观察筛选条件是否正常保留。 #### Does this PR introduce a user-facing change? ```release-note Console 端的文章管理列表支持在地址栏记录筛选条件。 ```pull/4212/head
parent
197096305b
commit
bb0a5f114a
|
@ -67,10 +67,10 @@
|
|||
"@uppy/status-bar": "^3.1.2",
|
||||
"@uppy/vue": "^1.0.2",
|
||||
"@uppy/xhr-upload": "^3.2.0",
|
||||
"@vueuse/components": "^9.6.0",
|
||||
"@vueuse/core": "^9.6.0",
|
||||
"@vueuse/router": "^9.6.0",
|
||||
"@vueuse/shared": "^9.6.0",
|
||||
"@vueuse/components": "^10.2.0",
|
||||
"@vueuse/core": "^10.2.0",
|
||||
"@vueuse/router": "^10.2.0",
|
||||
"@vueuse/shared": "^10.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"colorjs.io": "^0.4.3",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { IconArrowLeft, IconArrowRight } from "../../icons/icons";
|
||||
import { ref, watch } from "vue";
|
||||
import { useOffsetPagination } from "@vueuse/core";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -22,71 +21,69 @@ const props = withDefaults(
|
|||
}
|
||||
);
|
||||
|
||||
const page = ref(props.page);
|
||||
const size = ref(props.size);
|
||||
const total = ref(props.total);
|
||||
|
||||
watch([() => props.page, () => props.size, () => props.total], () => {
|
||||
page.value = props.page;
|
||||
size.value = props.size;
|
||||
total.value = props.total;
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:page", page: number): void;
|
||||
(event: "update:size", size: number): void;
|
||||
(event: "change", value: { page: number; size: number }): void;
|
||||
}>();
|
||||
|
||||
const onPageChange = ({
|
||||
currentPage,
|
||||
currentPageSize,
|
||||
}: {
|
||||
currentPage: number;
|
||||
currentPageSize: number;
|
||||
}) => {
|
||||
emit("update:page", currentPage);
|
||||
emit("update:size", currentPageSize);
|
||||
emit("change", {
|
||||
page: currentPage,
|
||||
size: currentPageSize,
|
||||
});
|
||||
const totalPages = computed(() => Math.ceil(props.total / props.size));
|
||||
|
||||
const hasNext = computed(() => props.page < totalPages.value);
|
||||
|
||||
const hasPrevious = computed(() => props.page > 1);
|
||||
|
||||
const onPageChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const page = Number(target.value);
|
||||
emit("update:page", page);
|
||||
emit("change", { page, size: props.size });
|
||||
};
|
||||
|
||||
const {
|
||||
currentPage,
|
||||
currentPageSize,
|
||||
pageCount,
|
||||
isFirstPage,
|
||||
isLastPage,
|
||||
prev,
|
||||
next,
|
||||
} = useOffsetPagination({
|
||||
total: total,
|
||||
page: page,
|
||||
pageSize: size,
|
||||
onPageChange: onPageChange,
|
||||
onPageSizeChange: onPageChange,
|
||||
});
|
||||
const onSizeChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const size = Number(target.value);
|
||||
emit("update:size", size);
|
||||
|
||||
// reset page to 1
|
||||
emit("update:page", 1);
|
||||
emit("change", { page: 1, size });
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
if (hasPrevious.value) {
|
||||
const page = props.page - 1;
|
||||
emit("update:page", page);
|
||||
emit("change", { page: page, size: props.size });
|
||||
}
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (hasNext.value) {
|
||||
const page = props.page + 1;
|
||||
emit("update:page", page);
|
||||
emit("change", { page: page, size: props.size });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="bg-white flex items-center justify-between">
|
||||
<div class="flex-1 flex justify-between sm:!hidden items-center">
|
||||
<span
|
||||
<button
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 cursor-pointer"
|
||||
@click="prev"
|
||||
:disabled="!hasPrevious"
|
||||
@click="previous"
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{{ currentPage }} / {{ pageCount }}
|
||||
</span>
|
||||
<span
|
||||
</button>
|
||||
<span class="text-sm text-gray-500"> {{ page }} / {{ totalPages }} </span>
|
||||
<button
|
||||
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 cursor-pointer"
|
||||
:disabled="!hasNext"
|
||||
@click="next"
|
||||
>
|
||||
<IconArrowRight />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center items-center gap-2">
|
||||
<nav
|
||||
|
@ -95,14 +92,14 @@ const {
|
|||
>
|
||||
<button
|
||||
class="relative h-8 outline-none inline-flex items-center px-2 py-1.5 rounded-l-base border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 cursor-pointer disabled:cursor-not-allowed"
|
||||
:disabled="isFirstPage"
|
||||
@click="prev"
|
||||
:disabled="!hasPrevious"
|
||||
@click="previous"
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</button>
|
||||
<button
|
||||
class="relative h-8 outline-none inline-flex items-center px-2 py-1.5 rounded-r-base border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 cursor-pointer disabled:cursor-not-allowed"
|
||||
:disabled="isLastPage"
|
||||
:disabled="!hasNext"
|
||||
@click="next"
|
||||
>
|
||||
<IconArrowRight />
|
||||
|
@ -110,21 +107,23 @@ const {
|
|||
</nav>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<select
|
||||
v-model="currentPage"
|
||||
:disabled="pageCount === 0"
|
||||
:value="page"
|
||||
:disabled="totalPages === 0"
|
||||
class="h-8 border outline-none rounded-base px-2 text-gray-800 text-sm border-gray-300"
|
||||
@change="onPageChange"
|
||||
>
|
||||
<option v-if="pageCount === 0" :value="0">0 / 0</option>
|
||||
<option v-for="i in pageCount" :key="i" :value="i">
|
||||
{{ i }} / {{ pageCount }}
|
||||
<option v-if="totalPages === 0" :value="0">0 / 0</option>
|
||||
<option v-for="i in totalPages" :key="i" :value="i">
|
||||
{{ i }} / {{ totalPages }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-sm text-gray-500">{{ pageLabel }}</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<select
|
||||
v-model="currentPageSize"
|
||||
:value="size"
|
||||
class="h-8 border outline-none rounded-base px-2 text-gray-800 text-sm border-gray-300"
|
||||
@change="onSizeChange"
|
||||
>
|
||||
<option
|
||||
v-for="(sizeOption, index) in sizeOptions"
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
|
@ -104,17 +108,17 @@ importers:
|
|||
specifier: ^3.2.0
|
||||
version: 3.2.0(@uppy/core@3.2.0)
|
||||
'@vueuse/components':
|
||||
specifier: ^9.6.0
|
||||
version: 9.6.0(vue@3.2.45)
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0(vue@3.2.45)
|
||||
'@vueuse/core':
|
||||
specifier: ^9.6.0
|
||||
version: 9.6.0(vue@3.2.45)
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0(vue@3.2.45)
|
||||
'@vueuse/router':
|
||||
specifier: ^9.6.0
|
||||
version: 9.6.0(vue-router@4.1.6)(vue@3.2.45)
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0(vue-router@4.1.6)(vue@3.2.45)
|
||||
'@vueuse/shared':
|
||||
specifier: ^9.6.0
|
||||
version: 9.6.0(vue@3.2.45)
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0(vue@3.2.45)
|
||||
axios:
|
||||
specifier: ^0.27.2
|
||||
version: 0.27.2
|
||||
|
@ -2781,6 +2785,11 @@ packages:
|
|||
resolution: {integrity: sha512-+lhQggrLvlQ/O5OmIYAc9gadcYXMoaDi0Doef+X/f6TLZFr9PTMjOpBWmpwNNHi026e54jckntUn6GzqDtIN4w==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
/@intlify/shared@9.3.0-beta.24:
|
||||
resolution: {integrity: sha512-AKxJ8s7eKIQWkNaf4wyyoLRwf4puCuQgjSChlDJm5JBEt6T8HGgnYTJLRXu6LD/JACn3Qwu6hM/XRX1c9yvjmQ==}
|
||||
engines: {node: '>= 16'}
|
||||
dev: true
|
||||
|
||||
/@intlify/unplugin-vue-i18n@0.9.3(rollup@2.79.1)(vue-i18n@9.3.0-beta.19):
|
||||
resolution: {integrity: sha512-23DMh2r0qA7UZfaQhF09ZHhifgTyKcbmVsCo+qHvu9q1EU8OF18VlhxMHMksDR5NBDvRXj3Lmu8lT84XDrUlSw==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
@ -2797,7 +2806,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
'@intlify/bundle-utils': 5.5.0(vue-i18n@9.3.0-beta.19)
|
||||
'@intlify/shared': 9.3.0-beta.19
|
||||
'@intlify/shared': 9.3.0-beta.24
|
||||
'@rollup/pluginutils': 5.0.2(rollup@2.79.1)
|
||||
'@vue/compiler-sfc': 3.2.47
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
|
@ -3998,8 +4007,8 @@ packages:
|
|||
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
||||
dev: false
|
||||
|
||||
/@types/web-bluetooth@0.0.16:
|
||||
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
|
||||
/@types/web-bluetooth@0.0.17:
|
||||
resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==}
|
||||
dev: false
|
||||
|
||||
/@types/yauzl@2.10.0:
|
||||
|
@ -4616,50 +4625,50 @@ packages:
|
|||
- typescript
|
||||
dev: true
|
||||
|
||||
/@vueuse/components@9.6.0(vue@3.2.45):
|
||||
resolution: {integrity: sha512-Jqv1g68PtBC8Nnp8u2rpf6qku8cslr381fvlY1uUZa75zI2imxPLglhOWA/dBtMjla4L3nmaf9S7PAzXJnwH9w==}
|
||||
/@vueuse/components@10.2.0(vue@3.2.45):
|
||||
resolution: {integrity: sha512-fpGtxx8G3BCJUoTd6d4xI7qELvm4nwVKLZYgIVdv7weqprKWwK5IO+t3LULovPuS7W2guVZgpyMy9NkD4qa2Bw==}
|
||||
dependencies:
|
||||
'@vueuse/core': 9.6.0(vue@3.2.45)
|
||||
'@vueuse/shared': 9.6.0(vue@3.2.45)
|
||||
vue-demi: 0.13.11(vue@3.2.45)
|
||||
'@vueuse/core': 10.2.0(vue@3.2.45)
|
||||
'@vueuse/shared': 10.2.0(vue@3.2.45)
|
||||
vue-demi: 0.14.5(vue@3.2.45)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/core@9.6.0(vue@3.2.45):
|
||||
resolution: {integrity: sha512-qGUcjKQXHgN+jqXEgpeZGoxdCbIDCdVPz3QiF1uyecVGbMuM63o96I1GjYx5zskKgRI0FKSNsVWM7rwrRMTf6A==}
|
||||
/@vueuse/core@10.2.0(vue@3.2.45):
|
||||
resolution: {integrity: sha512-aHBnoCteIS3hFu7ZZkVB93SanVDY6t4TIb7XDLxJT/HQdAZz+2RdIEJ8rj5LUoEJr7Damb5+sJmtpCwGez5ozQ==}
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.16
|
||||
'@vueuse/metadata': 9.6.0
|
||||
'@vueuse/shared': 9.6.0(vue@3.2.45)
|
||||
vue-demi: 0.13.11(vue@3.2.45)
|
||||
'@types/web-bluetooth': 0.0.17
|
||||
'@vueuse/metadata': 10.2.0
|
||||
'@vueuse/shared': 10.2.0(vue@3.2.45)
|
||||
vue-demi: 0.14.5(vue@3.2.45)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/metadata@9.6.0:
|
||||
resolution: {integrity: sha512-sIC8R+kWkIdpi5X2z2Gk8TRYzmczDwHRhEFfCu2P+XW2JdPoXrziqsGpDDsN7ykBx4ilwieS7JUIweVGhvZ93w==}
|
||||
/@vueuse/metadata@10.2.0:
|
||||
resolution: {integrity: sha512-IR7Mkq6QSgZ38q/2ZzOt+Zz1OpcEsnwE64WBumDQ+RGKrosFCtUA2zgRrOqDEzPBXrVB+4HhFkwDjQMu0fDBKw==}
|
||||
dev: false
|
||||
|
||||
/@vueuse/router@9.6.0(vue-router@4.1.6)(vue@3.2.45):
|
||||
resolution: {integrity: sha512-3TIZPX5smlimSNlTm+K3ESRTkA2VBHnwMintNrw4Z+WK5bh1UAh7lcBQluiGg3LJjkrMXYfuO7IPdU+a8NRnFA==}
|
||||
/@vueuse/router@10.2.0(vue-router@4.1.6)(vue@3.2.45):
|
||||
resolution: {integrity: sha512-vks2xHCZWeKaFWfgfmrzi6kJ0cTHvRCHzPUzmjvfbmzDrB2YOS1ymCPjfu1LH3E82H+RyebaSRhTeoDc3AsEFw==}
|
||||
peerDependencies:
|
||||
vue-router: '>=4.0.0-rc.1'
|
||||
dependencies:
|
||||
'@vueuse/shared': 9.6.0(vue@3.2.45)
|
||||
vue-demi: 0.13.11(vue@3.2.45)
|
||||
'@vueuse/shared': 10.2.0(vue@3.2.45)
|
||||
vue-demi: 0.14.5(vue@3.2.45)
|
||||
vue-router: 4.1.6(vue@3.2.45)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared@9.6.0(vue@3.2.45):
|
||||
resolution: {integrity: sha512-/eDchxYYhkHnFyrb00t90UfjCx94kRHxc7J1GtBCqCG4HyPMX+krV9XJgVtWIsAMaxKVU4fC8NSUviG1JkwhUQ==}
|
||||
/@vueuse/shared@10.2.0(vue@3.2.45):
|
||||
resolution: {integrity: sha512-dIeA8+g9Av3H5iF4NXR/sft4V6vys76CpZ6hxwj8eMXybXk2WRl3scSsOVi+kQ9SX38COR7AH7WwY83UcuxbSg==}
|
||||
dependencies:
|
||||
vue-demi: 0.13.11(vue@3.2.45)
|
||||
vue-demi: 0.14.5(vue@3.2.45)
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
@ -11288,6 +11297,21 @@ packages:
|
|||
vue: 3.2.45
|
||||
dev: false
|
||||
|
||||
/vue-demi@0.14.5(vue@3.2.45):
|
||||
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.0.0-rc.1
|
||||
vue: ^3.0.0-0 || ^2.6.0
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
dependencies:
|
||||
vue: 3.2.45
|
||||
dev: false
|
||||
|
||||
/vue-eslint-parser@9.3.0(eslint@8.43.0):
|
||||
resolution: {integrity: sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
|
|
|
@ -34,6 +34,7 @@ import { usePermission } from "@/utils/permission";
|
|||
import { postLabels } from "@/constants/labels";
|
||||
import { useMutation, useQuery } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
|
||||
import CategoryFilterDropdown from "@/components/filter/CategoryFilterDropdown.vue";
|
||||
import TagFilterDropdown from "@/components/filter/TagFilterDropdown.vue";
|
||||
|
@ -47,13 +48,24 @@ const checkedAll = ref(false);
|
|||
const selectedPostNames = ref<string[]>([]);
|
||||
|
||||
// Filters
|
||||
const selectedVisible = ref();
|
||||
const selectedPublishStatus = ref();
|
||||
const selectedSort = ref();
|
||||
const selectedCategory = ref();
|
||||
const selectedTag = ref();
|
||||
const selectedContributor = ref();
|
||||
const keyword = ref("");
|
||||
const page = useRouteQuery<number>("page", 1, {
|
||||
transform: Number,
|
||||
});
|
||||
const size = useRouteQuery<number>("size", 20, {
|
||||
transform: Number,
|
||||
});
|
||||
const selectedVisible = useRouteQuery<
|
||||
"PUBLIC" | "INTERNAL" | "PRIVATE" | undefined
|
||||
>("visible");
|
||||
const selectedPublishStatus = useRouteQuery<string | undefined>("status");
|
||||
const selectedSort = useRouteQuery<string | undefined>("sort");
|
||||
const selectedCategory = useRouteQuery<string | undefined>("category");
|
||||
const selectedTag = useRouteQuery<string | undefined>("tag");
|
||||
const selectedContributor = useRouteQuery<string | undefined>("contributor");
|
||||
const keyword = useRouteQuery<string>("keyword", "");
|
||||
const total = ref(0);
|
||||
const hasPrevious = ref(false);
|
||||
const hasNext = ref(false);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
|
@ -90,12 +102,6 @@ const hasFilters = computed(() => {
|
|||
);
|
||||
});
|
||||
|
||||
const page = ref(1);
|
||||
const size = ref(20);
|
||||
const total = ref(0);
|
||||
const hasPrevious = ref(false);
|
||||
const hasNext = ref(false);
|
||||
|
||||
const {
|
||||
data: posts,
|
||||
isLoading,
|
||||
|
@ -143,7 +149,7 @@ const {
|
|||
page: page.value,
|
||||
size: size.value,
|
||||
visible: selectedVisible.value,
|
||||
sort: [selectedSort.value?.sort].filter(Boolean) as string[],
|
||||
sort: [selectedSort.value].filter(Boolean) as string[],
|
||||
keyword: keyword.value,
|
||||
category: categories,
|
||||
tag: tags,
|
||||
|
|
Loading…
Reference in New Issue