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> <script lang="ts" setup>
import { setFocus } from "@/formkit/utils/focus"; import { setFocus } from "@/formkit/utils/focus";
import { useUserFetch } from "@console/modules/system/users/composables/use-user"; import {
import type { User } from "@halo-dev/api-client"; consoleApiClient,
coreApiClient,
type User,
} from "@halo-dev/api-client";
import { import {
IconArrowDown, IconArrowDown,
VAvatar, VAvatar,
@ -9,8 +12,9 @@ import {
VEntity, VEntity,
VEntityField, VEntityField,
} from "@halo-dev/components"; } from "@halo-dev/components";
import Fuse from "fuse.js"; import { useQuery } from "@tanstack/vue-query";
import { computed, ref, watch } from "vue"; import { refDebounced } from "@vueuse/shared";
import { ref, toRefs } from "vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -22,19 +26,70 @@ const props = withDefaults(
} }
); );
const { modelValue } = toRefs(props);
const emit = defineEmits<{ const emit = defineEmits<{
(event: "update:modelValue", value?: string): void; (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 dropdown = ref();
const handleSelect = (user: User) => { const handleSelect = (user: User) => {
if (user.metadata.name === props.modelValue) { const { name } = user.metadata || {};
if (name === props.modelValue) {
emit("update:modelValue", undefined); emit("update:modelValue", undefined);
} else { } else {
emit("update:modelValue", user.metadata.name); emit("update:modelValue", name);
} }
dropdown.value.hide(); dropdown.value.hide();
@ -45,34 +100,6 @@ function onDropdownShow() {
setFocus("userFilterDropdownInput"); setFocus("userFilterDropdownInput");
}, 200); }, 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> </script>
<template> <template>
@ -107,8 +134,9 @@ const selectedUser = computed(() => {
role="list" role="list"
> >
<li <li
v-for="(user, index) in searchResults" v-for="user in users"
:key="index" :key="user.metadata.name"
class="cursor-pointer"
@click="handleSelect(user)" @click="handleSelect(user)"
> >
<VEntity :is-selected="modelValue === user.metadata.name"> <VEntity :is-selected="modelValue === user.metadata.name">