Commit 93a107ee3b52b4917b047d06d22a60d111de1fbf
1 parent
c6b08f78
perf: tenant list tenant configuration use scroll select
Showing
5 changed files
with
206 additions
and
14 deletions
@@ -10,6 +10,7 @@ import { | @@ -10,6 +10,7 @@ import { | ||
10 | } from './tenantInfo'; | 10 | } from './tenantInfo'; |
11 | import { defHttp } from '/@/utils/http/axios'; | 11 | import { defHttp } from '/@/utils/http/axios'; |
12 | import { BasicPageParams } from '/@/api/model/baseModel'; | 12 | import { BasicPageParams } from '/@/api/model/baseModel'; |
13 | +import { PaginationResult } from '/#/axios'; | ||
13 | export type QueryTenantProfilesParam = BasicPageParams & OtherParams; | 14 | export type QueryTenantProfilesParam = BasicPageParams & OtherParams; |
14 | export type DeleteTenantProfilesParam = OtherParams; | 15 | export type DeleteTenantProfilesParam = OtherParams; |
15 | export type OtherParams = { | 16 | export type OtherParams = { |
@@ -47,7 +48,7 @@ export async function deleteTenantProfileApi(ids: string) { | @@ -47,7 +48,7 @@ export async function deleteTenantProfileApi(ids: string) { | ||
47 | } | 48 | } |
48 | 49 | ||
49 | export const getTableTenantProfileApi = (params?: QueryTenantProfilesParam) => { | 50 | export const getTableTenantProfileApi = (params?: QueryTenantProfilesParam) => { |
50 | - return defHttp.get({ | 51 | + return defHttp.get<PaginationResult>({ |
51 | url: Api.getTenantProfile, | 52 | url: Api.getTenantProfile, |
52 | params, | 53 | params, |
53 | }); | 54 | }); |
@@ -67,12 +68,12 @@ export const setTenantProfileIsDefaultApi = (id: string, v, params?: {}) => { | @@ -67,12 +68,12 @@ export const setTenantProfileIsDefaultApi = (id: string, v, params?: {}) => { | ||
67 | }; | 68 | }; |
68 | 69 | ||
69 | export const selectTenantProfileApi = async (params?: QueryTenantProfilesParam) => { | 70 | export const selectTenantProfileApi = async (params?: QueryTenantProfilesParam) => { |
70 | - const { items } = await getTableTenantProfileApi(params); | 71 | + const { items, total } = await getTableTenantProfileApi(params); |
71 | items.forEach((item) => { | 72 | items.forEach((item) => { |
72 | item.label = item.name; | 73 | item.label = item.name; |
73 | item.value = item.id.id; | 74 | item.value = item.id.id; |
74 | }); | 75 | }); |
75 | - return Promise.resolve<any[]>(items); | 76 | + return { items, total }; |
76 | }; | 77 | }; |
77 | 78 | ||
78 | export async function saveTenantProfileApi(params: tenantProfileDTO) { | 79 | export async function saveTenantProfileApi(params: tenantProfileDTO) { |
@@ -37,6 +37,7 @@ import ApiUpload from './components/ApiUpload.vue'; | @@ -37,6 +37,7 @@ import ApiUpload from './components/ApiUpload.vue'; | ||
37 | import ApiSearchSelect from './components/ApiSearchSelect.vue'; | 37 | import ApiSearchSelect from './components/ApiSearchSelect.vue'; |
38 | import CustomMinMaxInput from './externalCompns/components/CustomMinMaxInput.vue'; | 38 | import CustomMinMaxInput from './externalCompns/components/CustomMinMaxInput.vue'; |
39 | import StructForm from './externalCompns/components/StructForm/StructForm.vue'; | 39 | import StructForm from './externalCompns/components/StructForm/StructForm.vue'; |
40 | +import ApiSelectScrollLoad from './components/ApiSelectScrollLoad.vue'; | ||
40 | 41 | ||
41 | const componentMap = new Map<ComponentType, Component>(); | 42 | const componentMap = new Map<ComponentType, Component>(); |
42 | 43 | ||
@@ -81,6 +82,7 @@ componentMap.set('ApiUpload', ApiUpload); | @@ -81,6 +82,7 @@ componentMap.set('ApiUpload', ApiUpload); | ||
81 | componentMap.set('ApiSearchSelect', ApiSearchSelect); | 82 | componentMap.set('ApiSearchSelect', ApiSearchSelect); |
82 | componentMap.set('CustomMinMaxInput', CustomMinMaxInput); | 83 | componentMap.set('CustomMinMaxInput', CustomMinMaxInput); |
83 | componentMap.set('StructForm', StructForm); | 84 | componentMap.set('StructForm', StructForm); |
85 | +componentMap.set('ApiSelectScrollLoad', ApiSelectScrollLoad); | ||
84 | 86 | ||
85 | export function add(compName: ComponentType, component: Component) { | 87 | export function add(compName: ComponentType, component: Component) { |
86 | componentMap.set(compName, component); | 88 | componentMap.set(compName, component); |
1 | +<script lang="ts"> | ||
2 | + export default { | ||
3 | + inheritAttrs: true, | ||
4 | + }; | ||
5 | +</script> | ||
6 | +<script lang="ts" setup> | ||
7 | + import { ref, watchEffect, computed, unref, watch, reactive } from 'vue'; | ||
8 | + import { Select, Spin } from 'ant-design-vue'; | ||
9 | + import { isFunction } from '/@/utils/is'; | ||
10 | + import { useRuleFormItem } from '/@/hooks/component/useFormItem'; | ||
11 | + import { useAttrs } from '/@/hooks/core/useAttrs'; | ||
12 | + import { get, omit } from 'lodash-es'; | ||
13 | + import { LoadingOutlined } from '@ant-design/icons-vue'; | ||
14 | + import { useI18n } from '/@/hooks/web/useI18n'; | ||
15 | + import { PaginationResult } from '/#/axios'; | ||
16 | + import { useDebounceFn } from '@vueuse/core'; | ||
17 | + | ||
18 | + type OptionsItem = { label: string; value: string; disabled?: boolean }; | ||
19 | + | ||
20 | + type Pagination = Record<'page' | 'pageSize', number>; | ||
21 | + | ||
22 | + const emit = defineEmits(['options-change', 'change']); | ||
23 | + const props = withDefaults( | ||
24 | + defineProps<{ | ||
25 | + value?: Recordable | number | string; | ||
26 | + numberToString?: boolean; | ||
27 | + api?: (arg?: Recordable) => Promise<PaginationResult<OptionsItem>>; | ||
28 | + params?: Recordable; | ||
29 | + resultField?: string; | ||
30 | + labelField?: string; | ||
31 | + valueField?: string; | ||
32 | + immediate?: boolean; | ||
33 | + pagenation?: Pagination; | ||
34 | + queryEmptyDataAgin?: boolean; | ||
35 | + }>(), | ||
36 | + { | ||
37 | + resultField: '', | ||
38 | + labelField: 'label', | ||
39 | + valueField: 'value', | ||
40 | + immediate: true, | ||
41 | + queryEmptyDataAgin: true, | ||
42 | + pagenation: () => ({ page: 1, pageSize: 10 }), | ||
43 | + } | ||
44 | + ); | ||
45 | + | ||
46 | + const OptionsItem = (_, { attrs }: { attrs: { vNode: any } }) => attrs.vNode; | ||
47 | + | ||
48 | + const options = ref<OptionsItem[]>([]); | ||
49 | + const pagination = reactive(Object.assign({ total: 0, page: 1, pageSize: 10 }, props.pagenation)); | ||
50 | + const scrollLoading = ref(false); | ||
51 | + const lock = ref(false); | ||
52 | + const loading = ref(false); | ||
53 | + const isFirstLoad = ref(true); | ||
54 | + const emitData = ref<any[]>([]); | ||
55 | + const attrs = useAttrs(); | ||
56 | + const { t } = useI18n(); | ||
57 | + | ||
58 | + // Embedded in the form, just use the hook binding to perform form verification | ||
59 | + const [state] = useRuleFormItem(props, 'value', 'change', emitData); | ||
60 | + | ||
61 | + const getOptions = computed(() => { | ||
62 | + const { labelField, valueField, numberToString } = props; | ||
63 | + | ||
64 | + return unref(options).reduce((prev, next: Recordable) => { | ||
65 | + if (next) { | ||
66 | + const value = get(next, valueField); | ||
67 | + prev.push({ | ||
68 | + label: next[labelField], | ||
69 | + value: numberToString ? `${value}` : value, | ||
70 | + ...omit(next, [labelField, valueField]), | ||
71 | + }); | ||
72 | + } | ||
73 | + return prev; | ||
74 | + }, [] as OptionsItem[]); | ||
75 | + }); | ||
76 | + | ||
77 | + watchEffect(() => { | ||
78 | + props.immediate && isFirstLoad.value && fetch(); | ||
79 | + }); | ||
80 | + | ||
81 | + watch( | ||
82 | + () => props.params, | ||
83 | + () => { | ||
84 | + !unref(isFirstLoad) && fetch(); | ||
85 | + }, | ||
86 | + { deep: true } | ||
87 | + ); | ||
88 | + | ||
89 | + async function fetch() { | ||
90 | + const api = props.api; | ||
91 | + if (!api || !isFunction(api)) return; | ||
92 | + try { | ||
93 | + !unref(getOptions).length ? (loading.value = true) : (scrollLoading.value = true); | ||
94 | + lock.value = true; | ||
95 | + const { total, items } = await api({ | ||
96 | + ...props.params, | ||
97 | + page: pagination.page, | ||
98 | + pageSize: pagination.pageSize, | ||
99 | + }); | ||
100 | + | ||
101 | + pagination.total = total; | ||
102 | + if (Array.isArray(items)) { | ||
103 | + options.value = [...options.value, ...items]; | ||
104 | + emitChange(); | ||
105 | + return; | ||
106 | + } | ||
107 | + if (props.resultField) { | ||
108 | + options.value = [...options.value, ...(get(items, props.resultField) || [])]; | ||
109 | + } | ||
110 | + emitChange(); | ||
111 | + } catch (error) { | ||
112 | + pagination.page = Math.ceil(unref(getOptions).length / pagination.pageSize); | ||
113 | + console.warn(error); | ||
114 | + } finally { | ||
115 | + isFirstLoad.value = false; | ||
116 | + loading.value = false; | ||
117 | + scrollLoading.value = false; | ||
118 | + lock.value = false; | ||
119 | + } | ||
120 | + } | ||
121 | + | ||
122 | + async function handleFetch() { | ||
123 | + if (!props.immediate && unref(isFirstLoad)) { | ||
124 | + await fetch(); | ||
125 | + isFirstLoad.value = false; | ||
126 | + } | ||
127 | + } | ||
128 | + | ||
129 | + function emitChange() { | ||
130 | + emit('options-change', unref(getOptions)); | ||
131 | + } | ||
132 | + | ||
133 | + function handleChange(_, ...args) { | ||
134 | + emitData.value = args; | ||
135 | + } | ||
136 | + | ||
137 | + async function handlePopupScroll(event: MouseEvent) { | ||
138 | + const { scrollHeight, scrollTop, clientHeight } = event.target as HTMLDivElement; | ||
139 | + if (scrollTop + clientHeight >= scrollHeight) { | ||
140 | + if (unref(getOptions).length < pagination.total && !unref(lock)) { | ||
141 | + pagination.page = pagination.page + 1; | ||
142 | + await fetch(); | ||
143 | + } | ||
144 | + } | ||
145 | + } | ||
146 | + | ||
147 | + const debounceHandlePopupScroll = useDebounceFn(handlePopupScroll, 100); | ||
148 | +</script> | ||
149 | + | ||
150 | +<template> | ||
151 | + <Select | ||
152 | + @dropdownVisibleChange="handleFetch" | ||
153 | + v-bind="attrs" | ||
154 | + @change="handleChange" | ||
155 | + :options="getOptions" | ||
156 | + v-model:value="state" | ||
157 | + @popup-scroll="debounceHandlePopupScroll" | ||
158 | + > | ||
159 | + <template #[item]="data" v-for="item in Object.keys($slots)"> | ||
160 | + <slot :name="item" v-bind="data || {}"></slot> | ||
161 | + </template> | ||
162 | + <template #suffixIcon v-if="loading"> | ||
163 | + <LoadingOutlined spin /> | ||
164 | + </template> | ||
165 | + <template #notFoundContent v-if="loading"> | ||
166 | + <span> | ||
167 | + <LoadingOutlined spin class="mr-1" /> | ||
168 | + {{ t('component.form.apiSelectNotFound') }} | ||
169 | + </span> | ||
170 | + </template> | ||
171 | + <template #dropdownRender="{ menuNode }"> | ||
172 | + <OptionsItem :vNode="menuNode" /> | ||
173 | + <div v-show="scrollLoading" class="flex justify-center"> | ||
174 | + <Spin size="small" /> | ||
175 | + </div> | ||
176 | + </template> | ||
177 | + </Select> | ||
178 | +</template> |
1 | import { BasicColumn } from '/@/components/Table/src/types/table'; | 1 | import { BasicColumn } from '/@/components/Table/src/types/table'; |
2 | import { FormSchema } from '/@/components/Form'; | 2 | import { FormSchema } from '/@/components/Form'; |
3 | import { getAllRoleList } from '/@/api/system/system'; | 3 | import { getAllRoleList } from '/@/api/system/system'; |
4 | -import { selectTenantProfileApi } from '/@/api/tenant/tenantApi'; | 4 | +import { getTableTenantProfileApi, QueryTenantProfilesParam } from '/@/api/tenant/tenantApi'; |
5 | import { RoleEnum } from '/@/enums/roleEnum'; | 5 | import { RoleEnum } from '/@/enums/roleEnum'; |
6 | 6 | ||
7 | export function getBasicColumns(): BasicColumn[] { | 7 | export function getBasicColumns(): BasicColumn[] { |
@@ -123,16 +123,26 @@ export const tenantFormSchema: FormSchema[] = [ | @@ -123,16 +123,26 @@ export const tenantFormSchema: FormSchema[] = [ | ||
123 | { | 123 | { |
124 | field: 'tenantProfileId', | 124 | field: 'tenantProfileId', |
125 | label: '租户配置', | 125 | label: '租户配置', |
126 | - component: 'ApiSelect', | 126 | + component: 'ApiSelectScrollLoad', |
127 | required: true, | 127 | required: true, |
128 | - defaultValue: 'Default', | ||
129 | - componentProps: { | ||
130 | - api: selectTenantProfileApi, | ||
131 | - showSearch: true, | ||
132 | - params: { | ||
133 | - page: 1, | ||
134 | - pageSize: 100, | ||
135 | - }, | 128 | + componentProps: ({ formActionType }) => { |
129 | + const { setFieldsValue } = formActionType; | ||
130 | + return { | ||
131 | + api: async (params: QueryTenantProfilesParam) => { | ||
132 | + const { items, total } = await getTableTenantProfileApi(params); | ||
133 | + const firstRecord = items.at(0); | ||
134 | + if (firstRecord) { | ||
135 | + setFieldsValue({ tenantProfileId: firstRecord.id.id }); | ||
136 | + } | ||
137 | + return { items, total }; | ||
138 | + }, | ||
139 | + showSearch: true, | ||
140 | + labelField: 'name', | ||
141 | + valueField: 'id.id', | ||
142 | + filterOption: (inputValue: string, options: Record<'label' | 'value', string>) => { | ||
143 | + return options.label.toLowerCase().includes(inputValue.toLowerCase()); | ||
144 | + }, | ||
145 | + }; | ||
136 | }, | 146 | }, |
137 | }, | 147 | }, |
138 | { | 148 | { |