refactor: optimize user filter component to support remote search (#6529)

#### What type of PR is this?

/area ui
/kind feature
/milestone 2.19.0

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

重构用户筛选组件,支持远程搜索,避免在用户量大的时候产生性能问题。

<img width="383" alt="image" src="https://github.com/user-attachments/assets/3f878b0b-3da0-48fe-97ee-add115d23801">

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

```release-note
重构用户筛选组件,支持远程搜索。

```
pull/6536/head
Ryan Wang 2024-08-27 17:53:19 +08:00 committed by GitHub
parent 97257f9577
commit 25893c0386
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 65 additions and 37 deletions

View File

@ -1,7 +1,10 @@
<script lang="ts" setup>
import { setFocus } from "@/formkit/utils/focus";
import { useUserFetch } from "@console/modules/system/users/composables/use-user";
import type { User } from "@halo-dev/api-client";
import {
consoleApiClient,
coreApiClient,
type User,
} from "@halo-dev/api-client";
import {
IconArrowDown,
VAvatar,
@ -9,8 +12,9 @@ import {
VEntity,
VEntityField,
} from "@halo-dev/components";
import Fuse from "fuse.js";
import { computed, ref, watch } from "vue";
import { useQuery } from "@tanstack/vue-query";
import { refDebounced } from "@vueuse/shared";
import { ref, toRefs } from "vue";
const props = withDefaults(
defineProps<{
@ -22,19 +26,70 @@ const props = withDefaults(
}
);
const { modelValue } = toRefs(props);
const emit = defineEmits<{
(event: "update:modelValue", value?: string): void;
}>();
const { users } = useUserFetch({ fetchOnMounted: true });
const keyword = ref("");
const debouncedKeyword = refDebounced(keyword, 300);
const { data: selectedUser } = useQuery({
queryKey: ["core:users:by-name", modelValue],
queryFn: async () => {
if (!modelValue.value) {
return null;
}
const { data } = await coreApiClient.user.getUser({
name: modelValue.value,
});
return data;
},
cacheTime: 0,
});
const { data: users } = useQuery({
queryKey: ["core:users", debouncedKeyword],
queryFn: async () => {
const { data } = await consoleApiClient.user.listUsers({
fieldSelector: [`name!=anonymousUser`],
keyword: debouncedKeyword.value,
page: 1,
size: 30,
});
const pureUsers = data?.items?.map((user) => user.user);
if (!pureUsers?.length) {
return [selectedUser.value].filter(Boolean) as User[];
}
if (selectedUser.value) {
return [
selectedUser.value,
...pureUsers.filter(
(user) => user.metadata.name !== selectedUser.value?.metadata.name
),
];
}
return pureUsers;
},
staleTime: 2000,
});
const dropdown = ref();
const handleSelect = (user: User) => {
if (user.metadata.name === props.modelValue) {
const { name } = user.metadata || {};
if (name === props.modelValue) {
emit("update:modelValue", undefined);
} else {
emit("update:modelValue", user.metadata.name);
emit("update:modelValue", name);
}
dropdown.value.hide();
@ -45,34 +100,6 @@ function onDropdownShow() {
setFocus("userFilterDropdownInput");
}, 200);
}
// search
const keyword = ref("");
let fuse: Fuse<User> | undefined = undefined;
watch(
() => users.value,
() => {
fuse = new Fuse(users.value, {
keys: ["spec.displayName", "metadata.name", "spec.email"],
useExtendedSearch: true,
threshold: 0.2,
});
}
);
const searchResults = computed(() => {
if (!fuse || !keyword.value) {
return users.value;
}
return fuse?.search(keyword.value).map((item) => item.item);
});
const selectedUser = computed(() => {
return users.value.find((user) => user.metadata.name === props.modelValue);
});
</script>
<template>
@ -107,8 +134,9 @@ const selectedUser = computed(() => {
role="list"
>
<li
v-for="(user, index) in searchResults"
:key="index"
v-for="user in users"
:key="user.metadata.name"
class="cursor-pointer"
@click="handleSelect(user)"
>
<VEntity :is-selected="modelValue === user.metadata.name">