BasicCardList.vue 8.07 KB
<script setup lang="ts">
  import { BasicForm, FormActionType, useForm } from '/@/components/Form';
  import { List, ListProps, Button, Tooltip, DropdownButton, Menu } from 'ant-design-vue';
  import { computed, Ref, ref, toRaw, unref, useSlots, watch } from 'vue';
  import { useCardListScroll } from './hooks/useCardListScroll';
  import { BasicCardListPropsType, CardListActionType } from './types';
  import CardListLayout from './components/CardListLayout.vue';
  import { usePagination } from './hooks/usePagination';
  import { useLoading } from './hooks/useLoading';
  import { useCardListData } from './hooks/useCardListData';
  import { useListGrid } from './hooks/useListGrid';
  import { FETCH_SETTING } from '../../Table/src/const';
  import { useCardForm } from './hooks/useCardForm';
  import { Icon } from '/@/components/Icon';
  import { useCardListSelected } from './hooks/useCardListSelected';

  const emit = defineEmits<{
    (eventName: 'fetchSuccess', result: { items: Recordable[]; total: number }): void;
    (eventName: 'fetchError', error: Error): void;
    (
      eventName: 'register',
      cardListActionType: CardListActionType,
      formActions: FormActionType
    ): void;
  }>();

  const slots = useSlots();

  const props = withDefaults(defineProps<BasicCardListPropsType>(), {
    showCardListHeader: true,
    showCardListSetting: true,
    useSearchForm: false,
    gutter: 16,
    immediate: true,
    fetchSetting: () => FETCH_SETTING,
    autoCreateKey: true,
    offsetHeight: 0,
  });

  const tableData = ref<Recordable[]>([]);

  const listElRef = ref<Nullable<ComponentElRef>>(null);

  const innerPropsRef = ref<BasicCardListPropsType>();

  const [registerForm, formActions] = useForm();

  const getProps = computed<BasicCardListPropsType>(() => {
    return { ...props, ...(unref(innerPropsRef) || {}) } as BasicCardListPropsType;
  });

  const { getListGrid, cardListLayout } = useListGrid(getProps);

  const { getPaginationInfo, getPagination, setPagination } = usePagination(
    getProps,
    cardListLayout
  );

  const { loading, setLoading, getLoading } = useLoading();

  const {
    getDataSourceRef,
    getRowKey,
    fetch,
    reload,
    updateTableDataRecord,
    findCardDataRecord,
    setCardListData,
  } = useCardListData(getProps, formActions, {
    setLoading,
    getPaginationInfo,
    emit,
    tableData,
    setPagination,
  });

  const { redoHeight, containerHeight } = useCardListScroll(
    listElRef as Ref<Nullable<ComponentElRef>>,
    getProps,
    getDataSourceRef
  );

  const { replaceFormSlotKey, getFormProps, getFormSlotKeys } = useCardForm(
    getProps,
    slots,
    fetch,
    getLoading
  );

  const {
    handlerSelected,
    selectedClass,
    selectedKeys,
    selectAllToggle,
    selectedAll,
    clearSelectedKeys,
    getSelectedKeys,
    getSelectedRecords,
    getSelections,
  } = useCardListSelected(getDataSourceRef, getProps, {
    updateTableDataRecord,
    findCardDataRecord,
    getRowKey,
    setCardListData,
  });

  function handleCardListChange() {
    reload();
  }

  const getBindValues = computed<ListProps>(() => {
    const dataSource = unref(getDataSourceRef);

    const props = {
      dataSource,
      grid: unref(getListGrid),
      loading: unref(loading),
      pagination: {
        ...toRaw(unref(getPaginationInfo)),
        onChange: (current: number, pageSize: number) => {
          setPagination({ current, pageSize });
          handleCardListChange();
        },
      },
      rowKey: (record: Recordable) => record[unref(getRowKey)],
    } as ListProps;
    return props;
  });

  watch(cardListLayout, () => {
    const pageSize = cardListLayout.row * cardListLayout.col;
    setPagination({ pageSize });
    handleCardListChange();
  });

  const CartListActionType: CardListActionType = {
    setProps,
    setLoading,
    setPagination,
    getPagination,
    reload,
    selectedAll,
    clearSelectedKeys,
    getSelectedKeys,
    getSelectedRecords,
    getSelections,
  };

  function setProps(props: Partial<BasicCardListPropsType> = {}) {
    innerPropsRef.value = { ...(unref(innerPropsRef) || {}), ...props } as BasicCardListPropsType;
  }

  emit('register', CartListActionType, formActions);
</script>

<template>
  <main class="basic-card-list p-4 flex flex-col w-full h-full">
    <section v-if="getProps.useSearchForm" class="w-full bg-light-50 dark:bg-dark-900 p-4 mb-4">
      <BasicForm
        @register="registerForm"
        v-bind="getFormProps"
        @advanced-change="redoHeight"
        @reset="reload()"
        @submit="reload()"
      >
        <template #[replaceFormSlotKey(item)]="data" v-for="item in getFormSlotKeys">
          <slot :name="item" v-bind="data"></slot>
        </template>
      </BasicForm>
    </section>

    <section class="w-full bg-light-50 dark:bg-dark-900 p-4 flex-auto h-full">
      <List ref="listElRef" v-bind="getBindValues">
        <template #header>
          <section class="flex justify-between items-center">
            <div>
              <span v-if="getProps.title" class="font-bold text-base px-4">
                {{ getProps.title }}
              </span>
              <slot v-else name="title"></slot>
            </div>
            <div class="flex flex-auto justify-end mr-4 gap-4">
              <slot name="toolbar"></slot>
            </div>
            <div v-if="showCardListSetting" class="flex gap-4">
              <DropdownButton v-if="getProps.selections" @click="selectAllToggle" type="primary">
                <template #overlay>
                  <Menu>
                    <Menu.Item @click="selectedAll">全选</Menu.Item>
                    <Menu.Item @click="clearSelectedKeys">反选</Menu.Item>
                  </Menu>
                </template>
                <span>
                  {{ selectedKeys.length === getDataSourceRef.length ? '反选' : '全选' }}
                </span>
              </DropdownButton>
              <CardListLayout v-model:row="cardListLayout.row" v-model:col="cardListLayout.col" />
              <Tooltip title="刷新">
                <Button type="primary" @click="reload()" :loading="loading">
                  <Icon icon="ant-design:reload-outlined" :size="20" />
                </Button>
              </Tooltip>
            </div>
          </section>
        </template>
        <template #renderItem="{ item }: BasicCardListRenderItem<any>">
          <List.Item
            :class="selectedKeys.includes(item[getRowKey]) ? selectedClass : ''"
            @click="handlerSelected($event, item)"
          >
            <slot
              name="renderItem"
              :item="item"
              :totalHeight="containerHeight"
              :checked="item?.checked"
            >
            </slot>
          </List.Item>
        </template>
      </List>
    </section>
  </main>
</template>

<style lang="less" scoped>
  .basic-card-list {
    :deep(.ant-list) {
      .ant-spin-container {
        overflow-x: hidden;
        overflow-y: auto;

        .ant-list-item {
          border: 2px transparent solid;
          position: relative;

          &.basic-card-list-item-checked {
            border: 2px solid #377dff;

            &::after {
              position: absolute;
              inset-block-start: 2px;
              inset-inline-end: 2px;
              width: 0;
              height: 0;
              border: 10px solid #1677ff;
              border-block-end: 10px solid transparent;
              border-inline-start: 10px solid transparent;
              // border-start-end-radius: 6px;
              content: '';
            }

            &::before {
              content: '';
              position: absolute;
              background-color: #53a2fd;
              width: 100%;
              height: 100%;
              opacity: 0.2;
              z-index: 99;
            }
          }
        }
      }

      .ant-list-header {
        border-bottom: transparent;
      }

      .ant-list-pagination {
        margin-top: 16px;
        height: 25px;
      }

      .ant-list-empty-text {
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
  }
</style>