Commit 26254e832ffb733b8643d713e00aa540c4dc2e9e

Authored by ww
1 parent ee9b5b61

feat: 新增卡片列表组件

  1 +export { default as BasicCardList } from './src/BasicCardList.vue';
  2 +export { useCardList } from './src/hooks/useCardList';
  1 +<script setup lang="ts">
  2 + import { BasicForm, FormActionType, useForm } from '/@/components/Form';
  3 + import { List, ListProps, Button, Tooltip } from 'ant-design-vue';
  4 + import { computed, Ref, ref, toRaw, unref, useSlots, watch } from 'vue';
  5 + import { useCardListScroll } from './hooks/useCardListScroll';
  6 + import { BasicCardListPropsType, CardListActionType } from './types';
  7 + import CardListLayout from './components/CardListLayout.vue';
  8 + import { usePagination } from './hooks/usePagination';
  9 + import { useLoading } from './hooks/useLoading';
  10 + import { useCardListData } from './hooks/useCardListData';
  11 + import { useListGrid } from './hooks/useListGrid';
  12 + import { FETCH_SETTING } from '../../Table/src/const';
  13 + import { useCardForm } from './hooks/useCardForm';
  14 + import { Icon } from '/@/components/Icon';
  15 +
  16 + const emit = defineEmits<{
  17 + (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void;
  18 + (eventName: 'fetchError', error: Error): void;
  19 + (
  20 + eventName: 'register',
  21 + cardListActionType: CardListActionType,
  22 + formActions: FormActionType
  23 + ): void;
  24 + }>();
  25 +
  26 + const slots = useSlots();
  27 +
  28 + const props = withDefaults(defineProps<BasicCardListPropsType>(), {
  29 + showCardListHeader: true,
  30 + showCardListSetting: true,
  31 + useSearchForm: false,
  32 + gutter: 16,
  33 + immediate: true,
  34 + fetchSetting: () => FETCH_SETTING,
  35 + autoCreateKey: true,
  36 + });
  37 +
  38 + const tableData = ref<Recordable[]>([]);
  39 +
  40 + const listElRef = ref<Nullable<ComponentElRef>>(null);
  41 +
  42 + const innerPropsRef = ref<BasicCardListPropsType>();
  43 +
  44 + const [registerForm, formActions] = useForm();
  45 +
  46 + const getProps = computed<BasicCardListPropsType>(() => {
  47 + return { ...props, ...(unref(innerPropsRef) || {}) } as BasicCardListPropsType;
  48 + });
  49 +
  50 + const { getListGrid, cardListLayout } = useListGrid(getProps);
  51 +
  52 + const { getPaginationInfo, getPagination, setPagination } = usePagination(
  53 + getProps,
  54 + cardListLayout
  55 + );
  56 +
  57 + const { loading, setLoading, getLoading } = useLoading();
  58 +
  59 + const { getDataSourceRef, getRowKey, fetch, reload } = useCardListData(getProps, formActions, {
  60 + setLoading,
  61 + getPaginationInfo,
  62 + emit,
  63 + tableData,
  64 + setPagination,
  65 + });
  66 +
  67 + const { redoHeight, containerHeight } = useCardListScroll(
  68 + listElRef as Ref<Nullable<ComponentElRef>>,
  69 + getProps,
  70 + getDataSourceRef
  71 + );
  72 +
  73 + const { replaceFormSlotKey, getFormProps, getFormSlotKeys } = useCardForm(
  74 + getProps,
  75 + slots,
  76 + fetch,
  77 + getLoading
  78 + );
  79 +
  80 + function handleCardListChange() {
  81 + reload();
  82 + }
  83 +
  84 + const getBindData = computed<ListProps>(() => {
  85 + const dataSource = unref(getDataSourceRef);
  86 +
  87 + return {
  88 + dataSource,
  89 + grid: unref(getListGrid),
  90 + loading: unref(loading),
  91 + pagination: {
  92 + ...toRaw(unref(getPaginationInfo)),
  93 + onChange: (current: number, pageSize: number) => {
  94 + setPagination({ current, pageSize });
  95 + handleCardListChange();
  96 + },
  97 + },
  98 + rowKey: unref(getRowKey),
  99 + };
  100 + });
  101 +
  102 + watch(cardListLayout, () => {
  103 + const pageSize = cardListLayout.row * cardListLayout.col;
  104 + setPagination({ pageSize });
  105 + handleCardListChange();
  106 + });
  107 +
  108 + const CartListActionType: CardListActionType = {
  109 + setProps,
  110 + setLoading,
  111 + setPagination,
  112 + getPagination,
  113 + reload,
  114 + };
  115 +
  116 + function setProps(props: Partial<BasicCardListPropsType> = {}) {
  117 + innerPropsRef.value = { ...(unref(innerPropsRef) || {}), ...props } as BasicCardListPropsType;
  118 + }
  119 +
  120 + emit('register', CartListActionType, formActions);
  121 +</script>
  122 +
  123 +<template>
  124 + <main class="basic-card-list p-4 flex flex-col w-full h-full">
  125 + <section class="w-full bg-light-50 dark:bg-dark-900 p-4 mb-4">
  126 + <BasicForm
  127 + @register="registerForm"
  128 + v-bind="getFormProps"
  129 + @advanced-change="redoHeight"
  130 + @reset="reload()"
  131 + @submit="reload()"
  132 + >
  133 + <template #[replaceFormSlotKey(item)]="data" v-for="item in getFormSlotKeys">
  134 + <slot :name="item" v-bind="data"></slot>
  135 + </template>
  136 + </BasicForm>
  137 + </section>
  138 +
  139 + <section class="w-full bg-light-50 dark:bg-dark-900 p-4 flex-auto h-full">
  140 + <List ref="listElRef" v-bind="getBindData">
  141 + <template #header>
  142 + <section class="flex justify-between items-center">
  143 + <div>
  144 + <span v-if="title"></span>
  145 + <slot v-else name="name"></slot>
  146 + </div>
  147 + <div class="flex flex-auto justify-end mr-4">
  148 + <slot name="toolbar"></slot>
  149 + </div>
  150 + <div v-if="showCardListSetting" class="flex gap-4">
  151 + <CardListLayout v-model:row="cardListLayout.row" v-model:col="cardListLayout.col" />
  152 + <Tooltip title="刷新">
  153 + <Button type="primary" @click="reload()" :loading="loading">
  154 + <Icon icon="ant-design:reload-outlined" :size="20" />
  155 + </Button>
  156 + </Tooltip>
  157 + </div>
  158 + </section>
  159 + </template>
  160 + <template #renderItem="{ item }">
  161 + <List.Item>
  162 + <slot name="renderItem" :item="item" :totalHeight="containerHeight"></slot>
  163 + </List.Item>
  164 + </template>
  165 + </List>
  166 + </section>
  167 + </main>
  168 +</template>
  169 +
  170 +<style lang="less" scoped>
  171 + .basic-card-list {
  172 + :deep(.ant-list) {
  173 + .ant-spin-container {
  174 + overflow-x: hidden;
  175 + overflow-y: auto;
  176 +
  177 + .ant-row {
  178 + row-gap: 16px;
  179 +
  180 + .ant-list-item {
  181 + margin-bottom: 0;
  182 + }
  183 + }
  184 + }
  185 +
  186 + .ant-list-header {
  187 + border-bottom: transparent;
  188 + }
  189 +
  190 + .ant-list-pagination {
  191 + margin-top: 16px;
  192 + height: 25px;
  193 + }
  194 +
  195 + .ant-list-empty-text {
  196 + height: 100%;
  197 + display: flex;
  198 + align-items: center;
  199 + justify-content: center;
  200 + }
  201 + }
  202 + }
  203 +</style>
  1 +<script setup lang="ts">
  2 + import { Button, Popover } from 'ant-design-vue';
  3 + import { reactive, ref, watch } from 'vue';
  4 + import { Icon } from '/@/components/Icon';
  5 +
  6 + const emit = defineEmits(['update:row', 'update:col']);
  7 +
  8 + const props = withDefaults(
  9 + defineProps<{
  10 + row?: number;
  11 + col?: number;
  12 + }>(),
  13 + {
  14 + row: 2,
  15 + col: 5,
  16 + }
  17 + );
  18 +
  19 + const MAX_ROW = 4;
  20 + const MAX_COL = 9;
  21 +
  22 + const visible = ref(false);
  23 +
  24 + const selectedLayout = reactive({ row: props.row, col: props.col });
  25 +
  26 + const handleOver = (row: number, col: number) => {
  27 + Object.assign(selectedLayout, { row, col });
  28 + };
  29 +
  30 + const handleSelectConfirm = () => {
  31 + const { row, col } = selectedLayout;
  32 + emit('update:row', row);
  33 + emit('update:col', col);
  34 + visible.value = false;
  35 + };
  36 +
  37 + watch(
  38 + () => [props.row, props.col],
  39 + () => {
  40 + selectedLayout.row = props.row;
  41 + selectedLayout.col = props.col;
  42 + }
  43 + );
  44 +</script>
  45 +
  46 +<template>
  47 + <Popover v-model:visible="visible">
  48 + <template #content>
  49 + <section class="flex flex-wrap w-36">
  50 + <table class="border-collapse" @click="handleSelectConfirm">
  51 + <tr v-for="row in MAX_ROW" :key="row" class="border border-gray-300 border-solid">
  52 + <td
  53 + v-for="col in MAX_COL"
  54 + :class="selectedLayout.col >= col && selectedLayout.row >= row ? 'bg-blue-500' : ''"
  55 + :key="col"
  56 + class="w-4 h-4 border border-gray-300 border-solid cursor-pointer"
  57 + @mouseover="handleOver(row, col)"
  58 + >
  59 + </td>
  60 + </tr>
  61 + </table>
  62 + <div class="text-center w-full">
  63 + {{ selectedLayout.col }} x {{ selectedLayout.row }} 布局
  64 + </div>
  65 + </section>
  66 + </template>
  67 + <Button type="primary">
  68 + <Icon icon="ant-design:layout-filled" :size="20" />
  69 + </Button>
  70 + </Popover>
  71 +</template>
  1 +import type { ComputedRef, ExtractPropTypes, Slots } from 'vue';
  2 +import { unref, computed } from 'vue';
  3 +import { isFunction } from '/@/utils/is';
  4 +import { BasicCardListPropsType } from '../types';
  5 +import { FetchParams } from './useCardListData';
  6 +import { basicProps } from '/@/components/Form/src/props';
  7 +
  8 +export function useCardForm(
  9 + propsRef: ComputedRef<BasicCardListPropsType>,
  10 + slots: Slots,
  11 + fetch: (opt?: FetchParams | undefined) => Promise<void>,
  12 + getLoading: ComputedRef<boolean | undefined>
  13 +) {
  14 + const getFormProps = computed((): ExtractPropTypes<typeof basicProps> => {
  15 + const { formConfig } = unref(propsRef);
  16 + const { submitButtonOptions } = formConfig || {};
  17 + return {
  18 + showAdvancedButton: true,
  19 + ...formConfig,
  20 + submitButtonOptions: { loading: unref(getLoading), ...submitButtonOptions },
  21 + compact: true,
  22 + } as any;
  23 + });
  24 +
  25 + const getFormSlotKeys: ComputedRef<string[]> = computed(() => {
  26 + const keys = Object.keys(slots);
  27 + return keys
  28 + .map((item) => (item.startsWith('form-') ? item : null))
  29 + .filter((item) => !!item) as string[];
  30 + });
  31 +
  32 + function replaceFormSlotKey(key: string) {
  33 + if (!key) return '';
  34 + return key?.replace?.(/form\-/, '') ?? '';
  35 + }
  36 +
  37 + function handleSearchInfoChange(info: Recordable) {
  38 + const { handleSearchInfoFn } = unref(propsRef);
  39 + if (handleSearchInfoFn && isFunction(handleSearchInfoFn)) {
  40 + info = handleSearchInfoFn(info) || info;
  41 + }
  42 + fetch({ searchInfo: info, page: 1 });
  43 + }
  44 +
  45 + return {
  46 + getFormProps,
  47 + replaceFormSlotKey,
  48 + getFormSlotKeys,
  49 + handleSearchInfoChange,
  50 + };
  51 +}
  1 +import { WatchStopHandle, onUnmounted, ref, unref, watch } from 'vue';
  2 +import { BasicCardListPropsType, CardListActionType } from '../types';
  3 +import { isProdMode } from '/@/utils/env';
  4 +import { error } from '/@/utils/log';
  5 +import { FormActionType } from '/@/components/Form';
  6 +import { getDynamicProps } from '/@/utils';
  7 +import { PaginationProps } from 'ant-design-vue';
  8 +import { FetchParams } from './useCardListData';
  9 +
  10 +type UseTableMethod = CardListActionType & {
  11 + getForm: () => FormActionType;
  12 +};
  13 +
  14 +export function useCardList(
  15 + cardListProps?: BasicCardListPropsType
  16 +): [(instance: CardListActionType, formInstance: FormActionType) => void, CardListActionType] {
  17 + const cardListRef = ref<Nullable<CardListActionType>>(null);
  18 + const loadedRef = ref<Nullable<boolean>>(false);
  19 + const formRef = ref<Nullable<FormActionType>>(null);
  20 +
  21 + let stopWatch: WatchStopHandle;
  22 +
  23 + function register(instance: CardListActionType, formInstance: FormActionType) {
  24 + isProdMode() &&
  25 + onUnmounted(() => {
  26 + cardListRef.value = null;
  27 + loadedRef.value = null;
  28 + });
  29 +
  30 + if (unref(loadedRef) && isProdMode() && instance === unref(cardListRef)) return;
  31 +
  32 + cardListRef.value = instance;
  33 + formRef.value = formInstance;
  34 + cardListProps && instance.setProps(getDynamicProps(cardListProps));
  35 + loadedRef.value = true;
  36 +
  37 + stopWatch?.();
  38 +
  39 + stopWatch = watch(
  40 + () => cardListProps,
  41 + () => {
  42 + cardListProps && instance.setProps(getDynamicProps(cardListProps));
  43 + },
  44 + {
  45 + immediate: true,
  46 + deep: true,
  47 + }
  48 + );
  49 + }
  50 +
  51 + function getTableInstance(): CardListActionType {
  52 + const table = unref(cardListRef);
  53 + if (!table) {
  54 + error(
  55 + 'The CardList instance has not been obtained yet, please make sure the table is presented when performing the table operation!'
  56 + );
  57 + }
  58 + return table as CardListActionType;
  59 + }
  60 +
  61 + const cardListActionType: UseTableMethod = {
  62 + getForm: () => {
  63 + return unref(formRef) as unknown as FormActionType;
  64 + },
  65 + setLoading: (loading: boolean) => {
  66 + return getTableInstance().setLoading(loading);
  67 + },
  68 + setProps: (props: Partial<BasicCardListPropsType>) => {
  69 + getTableInstance().setProps(props);
  70 + },
  71 + getPagination: () => {
  72 + return getTableInstance().getPagination();
  73 + },
  74 + setPagination: (pagination: Partial<PaginationProps>) => {
  75 + return getTableInstance().setPagination(pagination);
  76 + },
  77 + reload: (opt?: FetchParams) => {
  78 + return getTableInstance().reload(opt);
  79 + },
  80 + };
  81 +
  82 + return [register, cardListActionType];
  83 +}
  1 +import { Ref, computed, onMounted, ref, unref, watch, watchEffect } from 'vue';
  2 +import { BasicCardListPropsType, CardListEmitType, UseLoading, UsePaginationType } from '../types';
  3 +import { FormActionType } from '/@/components/Form';
  4 +import { isBoolean, isFunction } from '/@/utils/is';
  5 +import { FETCH_SETTING, ROW_KEY } from '/@/components/Table/src/const';
  6 +import { PaginationProps } from 'ant-design-vue';
  7 +import { cloneDeep, get } from 'lodash-es';
  8 +import { buildUUID } from '/@/utils/uuid';
  9 +import { useTimeoutFn } from '/@/hooks/core/useTimeout';
  10 +
  11 +interface ActionType {
  12 + setLoading: UseLoading['setLoading'];
  13 + getPaginationInfo: UsePaginationType['getPaginationInfo'];
  14 + setPagination: UsePaginationType['setPagination'];
  15 + tableData: Ref<Recordable[]>;
  16 + emit: CardListEmitType;
  17 +}
  18 +
  19 +export interface FetchParams {
  20 + searchInfo?: Recordable;
  21 + page?: number;
  22 + sortInfo?: Recordable;
  23 + filterInfo?: Recordable;
  24 +}
  25 +
  26 +export function useCardListData(
  27 + propsRef: Ref<BasicCardListPropsType>,
  28 + formActionType: FormActionType,
  29 + actionType: ActionType
  30 +) {
  31 + const { getFieldsValue } = unref(formActionType)! || {};
  32 + const { getPaginationInfo, setLoading, setPagination, tableData, emit } = actionType;
  33 +
  34 + const dataSourceRef = ref<Recordable[]>([]);
  35 + const rawDataSourceRef = ref<Recordable>({});
  36 +
  37 + watchEffect(() => {
  38 + tableData.value = unref(dataSourceRef);
  39 + });
  40 +
  41 + watch(
  42 + () => unref(propsRef).dataSource,
  43 + () => {
  44 + const { dataSource, api } = unref(propsRef);
  45 + !api && dataSource && (dataSourceRef.value = dataSource);
  46 + },
  47 + {
  48 + immediate: true,
  49 + }
  50 + );
  51 +
  52 + function setTableKey(items: any[]) {
  53 + if (!items || !Array.isArray(items)) return;
  54 + items.forEach((item) => {
  55 + if (!item[ROW_KEY]) {
  56 + item[ROW_KEY] = buildUUID();
  57 + }
  58 + if (item.children && item.children.length) {
  59 + setTableKey(item.children);
  60 + }
  61 + });
  62 + }
  63 +
  64 + const getAutoCreateKey = computed(() => {
  65 + return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
  66 + });
  67 +
  68 + const getRowKey = computed(() => {
  69 + const { rowKey } = unref(propsRef);
  70 + return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
  71 + });
  72 +
  73 + const getDataSourceRef = computed(() => {
  74 + const dataSource = unref(dataSourceRef);
  75 + if (!dataSource || dataSource.length === 0) {
  76 + return unref(dataSourceRef);
  77 + }
  78 + if (unref(getAutoCreateKey)) {
  79 + const firstItem = dataSource[0];
  80 + const lastItem = dataSource[dataSource.length - 1];
  81 +
  82 + if (firstItem && lastItem) {
  83 + if (!firstItem[ROW_KEY] || !lastItem[ROW_KEY]) {
  84 + const data = cloneDeep(unref(dataSourceRef));
  85 + data.forEach((item) => {
  86 + if (!item[ROW_KEY]) {
  87 + item[ROW_KEY] = buildUUID();
  88 + }
  89 + if (item.children && item.children.length) {
  90 + setTableKey(item.children);
  91 + }
  92 + });
  93 + dataSourceRef.value = data;
  94 + }
  95 + }
  96 + }
  97 + return unref(dataSourceRef);
  98 + });
  99 +
  100 + async function fetch(opt?: FetchParams) {
  101 + const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm, pagination } =
  102 + unref(propsRef);
  103 +
  104 + if (!api || !isFunction(api)) return;
  105 +
  106 + try {
  107 + setLoading(true);
  108 + const { pageField, sizeField, listField, totalField } = Object.assign(
  109 + {},
  110 + FETCH_SETTING,
  111 + fetchSetting
  112 + );
  113 + let pageParams: Recordable = {};
  114 +
  115 + const { current = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
  116 +
  117 + if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
  118 + pageParams = {};
  119 + } else {
  120 + pageParams[pageField] = current;
  121 + pageParams[sizeField] = pageSize;
  122 + }
  123 +
  124 + let params: Recordable = {
  125 + ...pageParams,
  126 + ...(useSearchForm ? getFieldsValue() : {}),
  127 + ...searchInfo,
  128 + };
  129 +
  130 + if (beforeFetch && isFunction(beforeFetch)) {
  131 + params = (await beforeFetch(params)) || params;
  132 + }
  133 +
  134 + const res = await api(params);
  135 + rawDataSourceRef.value = res;
  136 +
  137 + const isArrayResult = Array.isArray(res);
  138 +
  139 + let resultItems: Recordable[] = isArrayResult ? res : get(res, listField);
  140 + const resultTotal: number = isArrayResult ? 0 : get(res, totalField);
  141 +
  142 + // 假如数据变少,导致总页数变少并小于当前选中页码,通过getPaginationRef获取到的页码是不正确的,需获取正确的页码再次执行
  143 + if (resultTotal) {
  144 + const currentTotalPage = Math.ceil(resultTotal / pageSize);
  145 + if (current > currentTotalPage) {
  146 + setPagination({
  147 + current: currentTotalPage,
  148 + });
  149 + fetch(opt);
  150 + }
  151 + }
  152 +
  153 + if (afterFetch && isFunction(afterFetch)) {
  154 + resultItems = (await afterFetch(resultItems, res)) || resultItems;
  155 + }
  156 + dataSourceRef.value = resultItems;
  157 + setPagination({
  158 + total: resultTotal || 0,
  159 + });
  160 + if (opt && opt.page) {
  161 + setPagination({
  162 + current: opt.page || 1,
  163 + });
  164 + }
  165 + emit('fetchSuccess', {
  166 + items: unref(resultItems),
  167 + total: resultTotal,
  168 + });
  169 + } catch (error) {
  170 + emit('fetchError', error as Error);
  171 + dataSourceRef.value = [];
  172 + setPagination({
  173 + total: 0,
  174 + });
  175 + } finally {
  176 + setLoading(false);
  177 + }
  178 + }
  179 +
  180 + async function reload(opt?: FetchParams) {
  181 + await fetch(opt);
  182 + }
  183 +
  184 + onMounted(() => {
  185 + useTimeoutFn(() => {
  186 + unref(propsRef).immediate && fetch();
  187 + }, 16);
  188 + });
  189 +
  190 + return {
  191 + reload,
  192 + fetch,
  193 + getRowKey,
  194 + getDataSourceRef,
  195 + };
  196 +}
  1 +import { Ref, ref, unref, watch } from 'vue';
  2 +import { getBoundingClientRect } from '/@/utils/domUtils';
  3 +import { BasicCardListPropsType } from '../types';
  4 +import { useDebounceFn } from '@vueuse/core';
  5 +
  6 +export function useCardListScroll(
  7 + cardListElRef: Ref<Nullable<ComponentElRef>>,
  8 + propsRef: Ref<BasicCardListPropsType>,
  9 + getDataSourceRef: Ref<Recordable[]>
  10 +) {
  11 + const containerHeight = ref(0);
  12 +
  13 + const redoHeight = () => {
  14 + const listEl = unref(cardListElRef)?.$el;
  15 + if (!listEl) return;
  16 +
  17 + const listContainerEl = listEl.querySelector('.ant-spin-container') as HTMLDivElement;
  18 +
  19 + if (!listContainerEl) return;
  20 +
  21 + const rect = getBoundingClientRect(listContainerEl);
  22 +
  23 + if (!rect) return;
  24 +
  25 + const { top } = rect as DOMRect;
  26 +
  27 + const { offsetHeight: otherOffsetHeight = 0 } = unref(propsRef);
  28 +
  29 + const totalHeight = document.documentElement.clientHeight;
  30 +
  31 + const offsetHeight = 32;
  32 +
  33 + const paginationHeight = 25 + 16;
  34 +
  35 + const residualHeight = totalHeight - top - offsetHeight - paginationHeight - otherOffsetHeight;
  36 +
  37 + listContainerEl.style.maxHeight = `${residualHeight}px`;
  38 + listContainerEl.style.height = `${residualHeight}px`;
  39 +
  40 + containerHeight.value = residualHeight;
  41 + };
  42 +
  43 + const debounceRedoHeight = useDebounceFn(redoHeight, 100);
  44 +
  45 + watch(
  46 + () => [unref(getDataSourceRef)?.length],
  47 + () => {
  48 + debounceRedoHeight();
  49 + },
  50 + {
  51 + flush: 'post',
  52 + }
  53 + );
  54 +
  55 + return { redoHeight, containerHeight };
  56 +}
  1 +import { Ref, computed, reactive, unref } from 'vue';
  2 +import { BasicCardListPropsType, ListGridType } from '../types';
  3 +import { getListGridByColumn } from '../utils';
  4 +
  5 +export function useListGrid(getProps: Ref<BasicCardListPropsType>) {
  6 + const cardListLayout = reactive({ row: 2, col: 5 });
  7 +
  8 + const getListGrid = computed<ListGridType>(() => {
  9 + const { col } = cardListLayout;
  10 + const { gutter = 16 } = unref(getProps);
  11 + return {
  12 + column: col,
  13 + gutter,
  14 + ...getListGridByColumn(col),
  15 + } as ListGridType;
  16 + });
  17 +
  18 + return {
  19 + getListGrid,
  20 + cardListLayout,
  21 + };
  22 +}
  1 +import { computed, ref, unref } from 'vue';
  2 +
  3 +export function useLoading() {
  4 + const loading = ref(false);
  5 +
  6 + const setLoading = (status: boolean) => (loading.value = status);
  7 +
  8 + const getLoading = computed(() => unref(loading));
  9 +
  10 + return {
  11 + loading,
  12 + getLoading,
  13 + setLoading,
  14 + };
  15 +}
  1 +import { PaginationProps } from 'ant-design-vue';
  2 +import { Ref, computed, ref, unref } from 'vue';
  3 +import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
  4 +import { BasicCardListPropsType } from '../types';
  5 +import { isBoolean } from '/@/utils/is';
  6 +
  7 +interface ItemRender {
  8 + page: number;
  9 + type: 'page' | 'prev' | 'next';
  10 + originalElement: any;
  11 +}
  12 +
  13 +function itemRender({ page, type, originalElement }: ItemRender) {
  14 + if (type === 'prev') {
  15 + return page === 0 ? null : <LeftOutlined />;
  16 + } else if (type === 'next') {
  17 + return page === 1 ? null : <RightOutlined />;
  18 + }
  19 + return originalElement;
  20 +}
  21 +
  22 +export function usePagination(
  23 + propsRef: Ref<BasicCardListPropsType>,
  24 + cardLayoutRef: Record<'row' | 'col', number>
  25 +) {
  26 + const configRef = ref<PaginationProps>({
  27 + hideOnSinglePage: false,
  28 + });
  29 +
  30 + const getPaginationInfo = computed(() => {
  31 + const { pagination } = unref(propsRef);
  32 + const { col, row } = cardLayoutRef;
  33 +
  34 + return {
  35 + current: 1,
  36 + pageSize: col * row,
  37 + size: 'small',
  38 + defaultPageSize: col * row,
  39 + showTotal: (total: number) => `共 ${total} 条数据`,
  40 + showSizeChanger: false,
  41 + itemRender: itemRender,
  42 + showQuickJumper: true,
  43 + ...(isBoolean(pagination) ? {} : pagination),
  44 + ...unref(configRef),
  45 + } as Partial<PaginationProps>;
  46 + });
  47 +
  48 + const setPagination = (info: Partial<PaginationProps>) => {
  49 + const paginationInfo = unref(getPaginationInfo);
  50 + configRef.value = {
  51 + ...(!isBoolean(paginationInfo) ? paginationInfo : {}),
  52 + ...info,
  53 + };
  54 + };
  55 +
  56 + function getPagination() {
  57 + return unref(getPaginationInfo);
  58 + }
  59 +
  60 + return { getPaginationInfo, setPagination, getPagination };
  61 +}
  1 +import { PaginationProps } from 'ant-design-vue';
  2 +import { FormProps } from '../../Form';
  3 +import { FetchSetting } from '../../Table';
  4 +import { useLoading } from './hooks/useLoading';
  5 +import { usePagination } from './hooks/usePagination';
  6 +import { FetchParams, useCardListData } from './hooks/useCardListData';
  7 +
  8 +// export interface CardList
  9 +
  10 +export interface BasicCardListPropsType<T = Recordable> {
  11 + title?: string;
  12 + formConfig?: FormProps;
  13 + showCardListSetting?: boolean;
  14 + useSearchForm?: boolean;
  15 + api?: (params?: any) => Promise<any>;
  16 + dataSource?: any[];
  17 + beforeFetch?: (params: FetchParams) => Promise<Recordable>;
  18 + pagination?: PaginationProps;
  19 + offsetHeight?: number;
  20 + gutter?: number;
  21 + searchInfo?: Recordable;
  22 + fetchSetting?: FetchSetting;
  23 + afterFetch?: (items: T[], result: any) => Promise<any[]>;
  24 + autoCreateKey?: boolean;
  25 + rowKey?: (item: T) => string | number;
  26 + immediate?: boolean;
  27 + handleSearchInfoFn?: Fn;
  28 +}
  29 +
  30 +export type ListGridType = Record<
  31 + 'column' | 'gutter' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl',
  32 + number
  33 +>;
  34 +
  35 +export type UseLoading = ReturnType<typeof useLoading>;
  36 +
  37 +export type UsePaginationType = ReturnType<typeof usePagination>;
  38 +export type UseCardListDataType = ReturnType<typeof useCardListData>;
  39 +
  40 +export interface CardListActionType {
  41 + setProps: (props: Partial<BasicCardListPropsType>) => void;
  42 + setLoading: UseLoading['setLoading'];
  43 + setPagination: UsePaginationType['setPagination'];
  44 + getPagination: UsePaginationType['getPagination'];
  45 + reload: UseCardListDataType['reload'];
  46 +}
  47 +
  48 +export interface CardListEmitType {
  49 + (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void;
  50 + (eventName: 'fetchError', error: Error): void;
  51 +}
  52 +
  53 +export interface CardListRenderItem<T = Recordable> {
  54 + item: T;
  55 + totalHeight: number;
  56 +}
  1 +import { ListGridType } from '../types';
  2 +
  3 +export const getListGridByColumn = (col: number): Omit<ListGridType, 'gutter' | 'column'> => {
  4 + return {
  5 + xxl: col,
  6 + xl: col,
  7 + lg: Math.min(col - 1, 2),
  8 + md: Math.min(col - 1, 2),
  9 + sm: Math.min(col - 2, 2),
  10 + xs: Math.min(col - 2, 2),
  11 + };
  12 +};
1 export { default as ModeSwitchButton } from './ModeSwitchButton.vue'; 1 export { default as ModeSwitchButton } from './ModeSwitchButton.vue';
2 export { default as CardLayoutButton } from './CardLayoutButton.vue'; 2 export { default as CardLayoutButton } from './CardLayoutButton.vue';
3 export { default as AuthIcon } from './AuthIcon.vue'; 3 export { default as AuthIcon } from './AuthIcon.vue';
  4 +export { default as AuthDropDown } from './AuthDropDown.vue';
4 export { 5 export {
5 EnumTableCardMode, 6 EnumTableCardMode,
6 EnumTableChartMode, 7 EnumTableChartMode,
  1 +import { WatchStopHandle, onUnmounted, ref, unref, watch } from 'vue';
  2 +import { OrganizationTreeActionType } from '../src/types';
  3 +import { isProdMode } from '/@/utils/env';
  4 +import { error } from '/@/utils/log';
  5 +import { OrganizationTreePropsType } from '../src/props';
  6 +import { getDynamicProps } from '/@/utils';
  7 +
  8 +export function useOrganizationTree(
  9 + organizationTreeProps?: Partial<OrganizationTreePropsType>
  10 +): [(instance: OrganizationTreeActionType) => void, OrganizationTreeActionType] {
  11 + const organizationTreeRef = ref<Nullable<OrganizationTreeActionType>>(null);
  12 + const loadedRef = ref<Nullable<boolean>>(false);
  13 + let stopWatch: WatchStopHandle;
  14 + function register(instance: OrganizationTreeActionType) {
  15 + isProdMode() &&
  16 + onUnmounted(() => {
  17 + organizationTreeRef.value = null;
  18 + loadedRef.value = null;
  19 + });
  20 +
  21 + if (unref(loadedRef) && isProdMode() && instance === unref(organizationTreeRef)) return;
  22 +
  23 + organizationTreeRef.value = instance;
  24 + loadedRef.value = true;
  25 +
  26 + stopWatch?.();
  27 +
  28 + stopWatch = watch(
  29 + () => organizationTreeProps,
  30 + () => {
  31 + organizationTreeProps && instance.setProps(getDynamicProps(organizationTreeProps));
  32 + },
  33 + {
  34 + immediate: true,
  35 + deep: true,
  36 + }
  37 + );
  38 + }
  39 +
  40 + function getOrganizationTreeInstance(): OrganizationTreeActionType {
  41 + const organizationTree = unref(organizationTreeRef);
  42 + if (!organizationTree) {
  43 + error(
  44 + 'The organization tree instance has not been obtained yet, please make sure the table is presented when performing the table operation!'
  45 + );
  46 + }
  47 + return organizationTree as OrganizationTreeActionType;
  48 + }
  49 +
  50 + const organizationTreeActionType: OrganizationTreeActionType = {
  51 + getSelectKey: () => getOrganizationTreeInstance().getSelectKey(),
  52 + clearSelected: () => getOrganizationTreeInstance().clearSelected(),
  53 + setProps: (props: Partial<OrganizationTreePropsType>) =>
  54 + getOrganizationTreeInstance().setProps(props),
  55 + };
  56 +
  57 + return [register, organizationTreeActionType];
  58 +}
1 import { useResetOrganizationTree } from './hooks/useOrganization'; 1 import { useResetOrganizationTree } from './hooks/useOrganization';
2 import OrganizationIdTree from './src/OrganizationIdTree.vue'; 2 import OrganizationIdTree from './src/OrganizationIdTree.vue';
  3 +export { useOrganizationTree } from './hooks/useOrganizationTree';
3 4
4 export { OrganizationIdTree, useResetOrganizationTree }; 5 export { OrganizationIdTree, useResetOrganizationTree };
@@ -17,41 +17,41 @@ @@ -17,41 +17,41 @@
17 </div> 17 </div>
18 </div> 18 </div>
19 <div :style="{ width: foldFlag ? '0px' : '100%' }" class="bg-white mr-0 overflow-hidden h-full"> 19 <div :style="{ width: foldFlag ? '0px' : '100%' }" class="bg-white mr-0 overflow-hidden h-full">
20 - <BasicTree  
21 - title="组织列表"  
22 - toolbar  
23 - search  
24 - :clickRowToExpand="false"  
25 - :treeData="treeData"  
26 - :expandedKeys="treeExpandData"  
27 - :replaceFields="{ key: 'id', title: 'name' }"  
28 - :selectedKeys="selectedKeys"  
29 - @select="handleSelect"  
30 - v-bind="$attrs"  
31 - /> 20 + <BasicTree v-bind="getBindData" />
32 </div> 21 </div>
33 </div> 22 </div>
34 </template> 23 </template>
35 <script lang="ts" setup name="OrganizationIdTree"> 24 <script lang="ts" setup name="OrganizationIdTree">
36 - import { onMounted, ref, unref } from 'vue'; 25 + import { computed, onMounted, ref, unref, useAttrs } from 'vue';
37 import { BasicTree, TreeItem } from '/@/components/Tree'; 26 import { BasicTree, TreeItem } from '/@/components/Tree';
38 import { getOrganizationList } from '/@/api/system/system'; 27 import { getOrganizationList } from '/@/api/system/system';
39 import { CaretRightOutlined } from '@ant-design/icons-vue'; 28 import { CaretRightOutlined } from '@ant-design/icons-vue';
40 import { getBoundingClientRect } from '/@/utils/domUtils'; 29 import { getBoundingClientRect } from '/@/utils/domUtils';
  30 + import { BasicTreePropsType, OrganizationTreeActionType } from './types';
  31 + import { OrganizationTreePropsType } from './props';
  32 +
  33 + const props = defineProps<OrganizationTreePropsType>();
  34 +
  35 + const attrs = useAttrs();
41 36
42 const tree = ref<Nullable<HTMLDivElement>>(); 37 const tree = ref<Nullable<HTMLDivElement>>();
43 - const emit = defineEmits(['select']); 38 + const emit = defineEmits(['select', 'register']);
44 const treeData = ref<TreeItem[]>([]); 39 const treeData = ref<TreeItem[]>([]);
45 const selectedKeys = ref<string[]>(); 40 const selectedKeys = ref<string[]>();
46 - const treeExpandData = ref([]); 41 + const treeExpandData = ref<string[]>([]);
  42 +
  43 + const innerProps = ref<OrganizationTreePropsType>({});
  44 +
47 //获取所有父级id 45 //获取所有父级id
48 - function findForAllId(data = [], arr = []) { 46 + function findForAllId(data: Recordable[] = [], arr: string[] = []) {
49 for (const item of data) { 47 for (const item of data) {
50 arr.push(item.id); 48 arr.push(item.id);
51 } 49 }
52 return arr; 50 return arr;
53 } 51 }
54 - function handleSelect(keys) { 52 +
  53 + function handleSelect(keys: string[]) {
  54 + selectedKeys.value = keys;
55 emit('select', keys[0]); 55 emit('select', keys[0]);
56 } 56 }
57 function resetOrganization() { 57 function resetOrganization() {
@@ -81,9 +81,41 @@ @@ -81,9 +81,41 @@
81 81
82 setTreeHeight(); 82 setTreeHeight();
83 }); 83 });
  84 +
84 defineExpose({ 85 defineExpose({
85 resetOrganization, 86 resetOrganization,
86 }); 87 });
  88 +
  89 + const getProps = computed<OrganizationTreePropsType>(() => ({ ...props, ...unref(innerProps) }));
  90 +
  91 + const getBindData = computed(() => {
  92 + return {
  93 + title: '组织列表',
  94 + toolbar: true,
  95 + search: true,
  96 + clickRowToExpand: false,
  97 + treeData: unref(treeData),
  98 + expandedKeys: unref(treeExpandData),
  99 + replaceFields: { key: 'id', title: 'name' },
  100 + selectedKeys: unref(selectedKeys),
  101 + ...attrs,
  102 + ...unref(getProps),
  103 + onSelect: (keys: string[]) => {
  104 + handleSelect(keys);
  105 + unref(getProps)?.onSelect?.(keys?.[0]);
  106 + },
  107 + } as BasicTreePropsType;
  108 + });
  109 +
  110 + const setProps = (props: Partial<OrganizationTreePropsType>) => {
  111 + innerProps.value = { ...(props || {}) };
  112 + };
  113 +
  114 + emit('register', {
  115 + clearSelected: resetOrganization,
  116 + getSelectKey: () => unref(selectedKeys)?.[0],
  117 + setProps,
  118 + } as OrganizationTreeActionType);
87 </script> 119 </script>
88 120
89 <style scoped lang="less"> 121 <style scoped lang="less">
  1 +export interface OrganizationTreePropsType {
  2 + onSelect?: (selectKeys: string) => any;
  3 +}
  1 +import { ExtractPropTypes } from 'vue';
  2 +import { basicProps } from '/@/components/Tree/src/props';
  3 +import { OrganizationTreePropsType } from './props';
  4 +
  5 +export interface OrganizationTreeActionType {
  6 + clearSelected(): void;
  7 + getSelectKey(): undefined | string;
  8 + setProps(props: Partial<OrganizationTreePropsType>): any;
  9 +}
  10 +
  11 +export type BasicTreePropsType = Partial<ExtractPropTypes<typeof basicProps>>;
1 import type { ComputedRef, Ref } from 'vue'; 1 import type { ComputedRef, Ref } from 'vue';
2 -import { DataActionModeEnum } from '/@/enums/toolEnum';  
3 2
4 -export type DynamicProps<T> = {  
5 - [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;  
6 -}; 3 +import { DataActionModeEnum } from '/@/enums/toolEnum';
7 4
8 export interface ModalParamsType<T = Recordable> { 5 export interface ModalParamsType<T = Recordable> {
9 mode: DataActionModeEnum; 6 mode: DataActionModeEnum;
@@ -11,9 +8,30 @@ export interface ModalParamsType<T = Recordable> { @@ -11,9 +8,30 @@ export interface ModalParamsType<T = Recordable> {
11 [key: string]: any; 8 [key: string]: any;
12 } 9 }
13 10
  11 +export type DynamicProps<T> = {
  12 + [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
  13 +};
  14 +
14 export interface DefineComponentsBasicExpose<T = Recordable> { 15 export interface DefineComponentsBasicExpose<T = Recordable> {
15 getFieldsValue: () => T; 16 getFieldsValue: () => T;
16 setFieldsValue: (value: T) => any; 17 setFieldsValue: (value: T) => any;
17 validate?: () => Promise<any>; 18 validate?: () => Promise<any>;
18 resetFieldsValue?: (...args) => any; 19 resetFieldsValue?: (...args) => any;
19 } 20 }
  21 +
  22 +declare global {
  23 + interface CardListRenderItem<T = Recordable> {
  24 + item: T;
  25 + totalHeight: number;
  26 + }
  27 +
  28 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  29 +
  30 + interface ModalParamsType<T = Recordable> {
  31 + mode: DataActionModeEnum;
  32 + record: T;
  33 + [key: string]: any;
  34 + }
  35 +}
  36 +
  37 +export {};