mirror of https://github.com/halo-dev/halo
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
parent
97257f9577
commit
25893c0386
|
@ -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">
|
||||||
|
|
Loading…
Reference in New Issue