Commit 93a107ee3b52b4917b047d06d22a60d111de1fbf

Authored by ww
1 parent c6b08f78

perf: tenant list tenant configuration use scroll select

... ... @@ -10,6 +10,7 @@ import {
10 10 } from './tenantInfo';
11 11 import { defHttp } from '/@/utils/http/axios';
12 12 import { BasicPageParams } from '/@/api/model/baseModel';
  13 +import { PaginationResult } from '/#/axios';
13 14 export type QueryTenantProfilesParam = BasicPageParams & OtherParams;
14 15 export type DeleteTenantProfilesParam = OtherParams;
15 16 export type OtherParams = {
... ... @@ -47,7 +48,7 @@ export async function deleteTenantProfileApi(ids: string) {
47 48 }
48 49
49 50 export const getTableTenantProfileApi = (params?: QueryTenantProfilesParam) => {
50   - return defHttp.get({
  51 + return defHttp.get<PaginationResult>({
51 52 url: Api.getTenantProfile,
52 53 params,
53 54 });
... ... @@ -67,12 +68,12 @@ export const setTenantProfileIsDefaultApi = (id: string, v, params?: {}) => {
67 68 };
68 69
69 70 export const selectTenantProfileApi = async (params?: QueryTenantProfilesParam) => {
70   - const { items } = await getTableTenantProfileApi(params);
  71 + const { items, total } = await getTableTenantProfileApi(params);
71 72 items.forEach((item) => {
72 73 item.label = item.name;
73 74 item.value = item.id.id;
74 75 });
75   - return Promise.resolve<any[]>(items);
  76 + return { items, total };
76 77 };
77 78
78 79 export async function saveTenantProfileApi(params: tenantProfileDTO) {
... ...
... ... @@ -37,6 +37,7 @@ import ApiUpload from './components/ApiUpload.vue';
37 37 import ApiSearchSelect from './components/ApiSearchSelect.vue';
38 38 import CustomMinMaxInput from './externalCompns/components/CustomMinMaxInput.vue';
39 39 import StructForm from './externalCompns/components/StructForm/StructForm.vue';
  40 +import ApiSelectScrollLoad from './components/ApiSelectScrollLoad.vue';
40 41
41 42 const componentMap = new Map<ComponentType, Component>();
42 43
... ... @@ -81,6 +82,7 @@ componentMap.set('ApiUpload', ApiUpload);
81 82 componentMap.set('ApiSearchSelect', ApiSearchSelect);
82 83 componentMap.set('CustomMinMaxInput', CustomMinMaxInput);
83 84 componentMap.set('StructForm', StructForm);
  85 +componentMap.set('ApiSelectScrollLoad', ApiSelectScrollLoad);
84 86
85 87 export function add(compName: ComponentType, component: Component) {
86 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>
... ...
... ... @@ -118,4 +118,5 @@ export type ComponentType =
118 118 | 'IconDrawer'
119 119 | 'ApiUpload'
120 120 | 'ApiSearchSelect'
121   - | 'StructForm';
  121 + | 'StructForm'
  122 + | 'ApiSelectScrollLoad';
... ...
1 1 import { BasicColumn } from '/@/components/Table/src/types/table';
2 2 import { FormSchema } from '/@/components/Form';
3 3 import { getAllRoleList } from '/@/api/system/system';
4   -import { selectTenantProfileApi } from '/@/api/tenant/tenantApi';
  4 +import { getTableTenantProfileApi, QueryTenantProfilesParam } from '/@/api/tenant/tenantApi';
5 5 import { RoleEnum } from '/@/enums/roleEnum';
6 6
7 7 export function getBasicColumns(): BasicColumn[] {
... ... @@ -123,16 +123,26 @@ export const tenantFormSchema: FormSchema[] = [
123 123 {
124 124 field: 'tenantProfileId',
125 125 label: '租户配置',
126   - component: 'ApiSelect',
  126 + component: 'ApiSelectScrollLoad',
127 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 {
... ...