Pārlūkot izejas kodu

附件组件单独封装。。

wangwei 4 gadi atpakaļ
vecāks
revīzija
a5361950da
51 mainītis faili ar 5224 papildinājumiem un 42 dzēšanām
  1. 14 0
      src/components/EasyTable/index.ts
  2. 391 0
      src/components/EasyTable/src/EasyTable.vue
  3. 36 0
      src/components/EasyTable/src/componentMap.ts
  4. 20 0
      src/components/EasyTable/src/components/EditTableHeaderIcon.vue
  5. 19 0
      src/components/EasyTable/src/components/ExpandIcon.tsx
  6. 49 0
      src/components/EasyTable/src/components/HeaderCell.vue
  7. 184 0
      src/components/EasyTable/src/components/TableAction.vue
  8. 95 0
      src/components/EasyTable/src/components/TableFooter.vue
  9. 74 0
      src/components/EasyTable/src/components/TableHeader.vue
  10. 86 0
      src/components/EasyTable/src/components/TableImg.vue
  11. 53 0
      src/components/EasyTable/src/components/TableTitle.vue
  12. 33 0
      src/components/EasyTable/src/components/editable/CellComponent.ts
  13. 412 0
      src/components/EasyTable/src/components/editable/EditableCell.vue
  14. 28 0
      src/components/EasyTable/src/components/editable/helper.ts
  15. 55 0
      src/components/EasyTable/src/components/editable/index.ts
  16. 453 0
      src/components/EasyTable/src/components/settings/ColumnSetting.vue
  17. 40 0
      src/components/EasyTable/src/components/settings/FullScreenSetting.vue
  18. 34 0
      src/components/EasyTable/src/components/settings/RedoSetting.vue
  19. 37 0
      src/components/EasyTable/src/components/settings/ShowFormSearch.vue
  20. 66 0
      src/components/EasyTable/src/components/settings/SizeSetting.vue
  21. 73 0
      src/components/EasyTable/src/components/settings/index.vue
  22. 26 0
      src/components/EasyTable/src/const.ts
  23. 332 0
      src/components/EasyTable/src/hooks/useColumns.ts
  24. 92 0
      src/components/EasyTable/src/hooks/useCustomRow.ts
  25. 297 0
      src/components/EasyTable/src/hooks/useDataSource.ts
  26. 21 0
      src/components/EasyTable/src/hooks/useLoading.ts
  27. 76 0
      src/components/EasyTable/src/hooks/usePagination.tsx
  28. 93 0
      src/components/EasyTable/src/hooks/useRowSelection.ts
  29. 144 0
      src/components/EasyTable/src/hooks/useTable.ts
  30. 23 0
      src/components/EasyTable/src/hooks/useTableContext.ts
  31. 57 0
      src/components/EasyTable/src/hooks/useTableFooter.ts
  32. 51 0
      src/components/EasyTable/src/hooks/useTableHeader.ts
  33. 178 0
      src/components/EasyTable/src/hooks/useTableScroll.ts
  34. 19 0
      src/components/EasyTable/src/hooks/useTableStyle.ts
  35. 130 0
      src/components/EasyTable/src/props.ts
  36. 198 0
      src/components/EasyTable/src/types/column.ts
  37. 9 0
      src/components/EasyTable/src/types/componentType.ts
  38. 99 0
      src/components/EasyTable/src/types/pagination.ts
  39. 441 0
      src/components/EasyTable/src/types/table.ts
  40. 24 0
      src/components/EasyTable/src/types/tableAction.ts
  41. 4 0
      src/components/Form/src/componentMap.ts
  42. 93 0
      src/components/Form/src/components/upload/ChooseModal.vue
  43. 61 0
      src/components/Form/src/components/upload/Image.vue
  44. 207 0
      src/components/Form/src/components/upload/UploadFile.vue
  45. 180 0
      src/components/Form/src/components/upload/UploadImage.vue
  46. 98 0
      src/components/Form/src/components/upload/data.ts
  47. 2 0
      src/components/Form/src/types/index.ts
  48. 4 10
      src/views/activity/activity/data.ts
  49. 4 10
      src/views/activity/meeting/data.ts
  50. 5 12
      src/views/bill/bill/data.ts
  51. 4 10
      src/views/content/content/data.ts

+ 14 - 0
src/components/EasyTable/index.ts

@@ -0,0 +1,14 @@
+export { default as EasyTable } from './src/EasyTable.vue';
+export { default as TableAction } from './src/components/TableAction.vue';
+export { default as EditTableHeaderIcon } from './src/components/EditTableHeaderIcon.vue';
+export { default as TableImg } from './src/components/TableImg.vue';
+
+export * from './src/types/table';
+export * from './src/types/pagination';
+export * from './src/types/tableAction';
+
+export { useTable } from './src/hooks/useTable';
+
+export type { FormSchema, FormProps } from '/@/components/Form/src/types/form';
+
+export type { EditRecordRow } from './src/components/editable';

+ 391 - 0
src/components/EasyTable/src/EasyTable.vue

@@ -0,0 +1,391 @@
+<template>
+  <div ref="wrapRef" :class="getWrapperClass">
+    <Table
+      ref="tableElRef"
+      class="mytable"
+      v-bind="getBindValues"
+      :rowClassName="getRowClassName"
+      v-show="getEmptyDataIsShowTable"
+      @change="handleTableChange"
+    >
+      <template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
+        <slot :name="item" v-bind="data"></slot>
+      </template>
+
+      <template #[`header-${column.dataIndex}`] v-for="column in columns" :key="column.dataIndex">
+        <HeaderCell :column="column" />
+      </template>
+    </Table>
+  </div>
+</template>
+<script lang="ts">
+  import type {
+    BasicTableProps,
+    TableActionType,
+    SizeType,
+    ColumnChangeParam,
+  } from './types/table';
+
+  import { defineComponent, ref, computed, unref, toRaw, onMounted, reactive, toRefs } from 'vue';
+  import { Table } from 'ant-design-vue';
+  import expandIcon from './components/ExpandIcon';
+  import HeaderCell from './components/HeaderCell.vue';
+  import { InnerHandlers } from './types/table';
+
+  import { usePagination } from './hooks/usePagination';
+  import { useColumns } from './hooks/useColumns';
+  import { useDataSource } from './hooks/useDataSource';
+  import { useLoading } from './hooks/useLoading';
+  import { useRowSelection } from './hooks/useRowSelection';
+  import { useTableScroll } from './hooks/useTableScroll';
+  import { useCustomRow } from './hooks/useCustomRow';
+  import { useTableStyle } from './hooks/useTableStyle';
+  import { useTableHeader } from './hooks/useTableHeader';
+  import { createTableContext } from './hooks/useTableContext';
+  import { useTableFooter } from './hooks/useTableFooter';
+  import { useExpose } from '/@/hooks/core/useExpose';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { omit } from 'lodash-es';
+  import { basicProps } from './props';
+  import { isFunction } from '/@/utils/is';
+
+  export default defineComponent({
+    components: {
+      Table,
+      HeaderCell,
+    },
+    props: basicProps,
+    emits: [
+      'fetch-success',
+      'fetch-error',
+      'selection-change',
+      'register',
+      'dragRow',
+      'row-click',
+      'row-dbClick',
+      'row-contextmenu',
+      'row-mouseenter',
+      'row-mouseleave',
+      'edit-end',
+      'edit-cancel',
+      'edit-row-end',
+      'edit-change',
+      'expanded-rows-change',
+      'change',
+      'columns-change',
+    ],
+    setup(props, { attrs, emit, slots }) {
+      const formState = reactive({
+        showSearch: false,
+      });
+      const tableElRef = ref<ComponentRef>(null);
+      const tableData = ref<Recordable[]>([]);
+
+      const wrapRef = ref<Nullable<HTMLDivElement>>(null);
+      const innerPropsRef = ref<Partial<BasicTableProps>>();
+
+      const { prefixCls } = useDesign('basic-table');
+
+      const getProps = computed(() => {
+        return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
+      });
+
+      const { getLoading, setLoading } = useLoading(getProps);
+      const {
+        getPaginationInfo,
+        getPagination,
+        setPagination,
+        setShowPagination,
+        getShowPagination,
+      } = usePagination(getProps);
+
+      const {
+        getRowSelection,
+        getRowSelectionRef,
+        getSelectRows,
+        clearSelectedRowKeys,
+        getSelectRowKeys,
+        deleteSelectRowByKey,
+        setSelectedRowKeys,
+      } = useRowSelection(getProps, tableData, emit);
+
+      const {
+        handleTableChange: onTableChange,
+        getDataSourceRef,
+        getDataSource,
+        setTableData,
+        updateTableDataRecord,
+        getRowKey,
+        getAutoCreateKey,
+        updateTableData,
+      } = useDataSource(
+        getProps,
+        {
+          tableData,
+          getPaginationInfo,
+          setLoading,
+          setPagination,
+          clearSelectedRowKeys,
+        },
+        emit
+      );
+
+      function handleTableChange(...args) {
+        onTableChange.call(undefined, ...args);
+        emit('change', ...args);
+        // 解决通过useTable注册onChange时不起作用的问题
+        const { onChange } = unref(getProps);
+        onChange && isFunction(onChange) && onChange.call(undefined, ...args);
+      }
+
+      const {
+        getViewColumns,
+        getColumns,
+        setCacheColumnsByField,
+        setColumns,
+        getColumnsRef,
+        getCacheColumns,
+      } = useColumns(getProps, getPaginationInfo);
+
+      const { getScrollRef, redoHeight } = useTableScroll(
+        getProps,
+        tableElRef,
+        getColumnsRef,
+        getRowSelectionRef,
+        getDataSourceRef
+      );
+
+      const { customRow } = useCustomRow(getProps, {
+        setSelectedRowKeys,
+        getSelectRowKeys,
+        clearSelectedRowKeys,
+        getAutoCreateKey,
+        emit,
+      });
+
+      const { getRowClassName } = useTableStyle(getProps, prefixCls);
+
+      const handlers: InnerHandlers = {
+        onColumnsChange: (data: ColumnChangeParam[]) => {
+          emit('columns-change', data);
+          // support useTable
+          unref(getProps).onColumnsChange?.(data);
+        },
+      };
+
+      const { getHeaderProps } = useTableHeader(getProps, slots, handlers);
+
+      const { getFooterProps } = useTableFooter(
+        getProps,
+        getScrollRef,
+        tableElRef,
+        getDataSourceRef
+      );
+
+      const getBindValues = computed(() => {
+        const dataSource = unref(getDataSourceRef);
+        let propsData: Recordable = {
+          size: 'middle',
+          // ...(dataSource.length === 0 ? { getPopupContainer: () => document.body } : {}),
+          ...attrs,
+          customRow,
+          expandIcon: expandIcon(),
+          ...unref(getProps),
+          ...unref(getHeaderProps),
+          scroll: unref(getScrollRef),
+          loading: unref(getLoading),
+          tableLayout: 'fixed',
+          rowSelection: unref(getRowSelectionRef),
+          rowKey: unref(getRowKey),
+          columns: toRaw(unref(getViewColumns)),
+          pagination: toRaw(unref(getPaginationInfo)),
+          dataSource,
+          footer: unref(getFooterProps),
+        };
+        if (slots.expandedRowRender) {
+          propsData = omit(propsData, 'scroll');
+        }
+
+        propsData = omit(propsData, ['class', 'onChange']);
+        return propsData;
+      });
+
+      const getWrapperClass = computed(() => {
+        const values = unref(getBindValues);
+        // 显示 表格搜索字段
+        // formState.showSearch = values.useSearchForm;
+        return [
+          prefixCls,
+          attrs.class,
+          {
+            [`${prefixCls}-form-container`]: values.useSearchForm,
+            [`${prefixCls}--inset`]: values.inset,
+          },
+        ];
+      });
+
+      const getEmptyDataIsShowTable = computed(() => {
+        const { emptyDataIsShowTable, useSearchForm } = unref(getProps);
+        if (emptyDataIsShowTable || !useSearchForm) {
+          return true;
+        }
+        return !!unref(getDataSourceRef).length;
+      });
+
+      function setProps(props: Partial<BasicTableProps>) {
+        innerPropsRef.value = { ...unref(innerPropsRef), ...props };
+      }
+
+      const tableAction: TableActionType = {
+        getSelectRows,
+        clearSelectedRowKeys,
+        getSelectRowKeys,
+        deleteSelectRowByKey,
+        setPagination,
+        setTableData,
+        updateTableDataRecord,
+        redoHeight,
+        setSelectedRowKeys,
+        setColumns,
+        setLoading,
+        getDataSource,
+        setProps,
+        getRowSelection,
+        getPaginationRef: getPagination,
+        getColumns,
+        getCacheColumns,
+        emit,
+        updateTableData,
+        setShowPagination,
+        getShowPagination,
+        setCacheColumnsByField,
+        getSize: () => {
+          return unref(getBindValues).size as SizeType;
+        },
+        showTableSearch: () => {
+          formState.showSearch = !formState.showSearch;
+          // 重新调整表格高度
+          redoHeight();
+        },
+      };
+      createTableContext({ ...tableAction, wrapRef, getBindValues });
+
+      useExpose<TableActionType>(tableAction);
+
+      emit('register', tableAction);
+
+      onMounted(() => {
+        if (!props.canDragRow) {
+          return;
+        }
+      });
+      return {
+        tableElRef,
+        getBindValues,
+        getLoading,
+        getEmptyDataIsShowTable,
+        handleTableChange,
+        getRowClassName,
+        wrapRef,
+        tableAction,
+        redoHeight,
+        getWrapperClass,
+        columns: getViewColumns,
+        ...toRefs(formState),
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @border-color: #cecece4d;
+
+  @prefix-cls: ~'@{namespace}-basic-table';
+
+  .@{prefix-cls} {
+    max-width: 100%;
+
+    &-row__striped {
+      td {
+        background-color: @app-content-background;
+      }
+    }
+
+    &-form-container {
+      padding: 16px;
+
+      .ant-form {
+        padding: 12px 10px 6px 10px;
+        margin-bottom: 16px;
+        background-color: @component-background;
+        border-radius: 2px;
+      }
+    }
+
+    &--inset {
+      .ant-table-wrapper {
+        padding: 0;
+      }
+    }
+
+    .ant-tag {
+      margin-right: 0;
+    }
+
+    .ant-table-wrapper {
+      padding: 6px;
+      background-color: @component-background;
+      border-radius: 2px;
+
+      .ant-table-title {
+        min-height: 40px;
+        padding: 0 0 8px 0 !important;
+      }
+
+      .ant-table.ant-table-bordered .ant-table-title {
+        border: none !important;
+      }
+    }
+
+    .ant-table {
+      width: 100%;
+      overflow-x: hidden;
+
+      &-title {
+        display: flex;
+        padding: 8px 6px;
+        border-bottom: none;
+        justify-content: space-between;
+        align-items: center;
+      }
+
+      .ant-table-tbody > tr.ant-table-row-selected td {
+        background-color: fade(@primary-color, 8%) !important;
+      }
+    }
+
+    .ant-pagination {
+      margin: 10px 0 0 0;
+    }
+
+    .ant-table-footer {
+      padding: 0;
+
+      .ant-table-wrapper {
+        padding: 0;
+      }
+
+      table {
+        border: none !important;
+      }
+
+      .ant-table-body {
+        overflow-x: hidden !important;
+        overflow-y: scroll !important;
+      }
+
+      td {
+        padding: 12px 8px;
+      }
+    }
+  }
+</style>

+ 36 - 0
src/components/EasyTable/src/componentMap.ts

@@ -0,0 +1,36 @@
+import type { Component } from 'vue';
+
+import {
+  Input,
+  Select,
+  Checkbox,
+  InputNumber,
+  Switch,
+  DatePicker,
+  TimePicker,
+} from 'ant-design-vue';
+
+import type { ComponentType } from './types/componentType';
+import { ApiSelect } from '/@/components/Form';
+
+const componentMap = new Map<ComponentType, Component>();
+
+componentMap.set('Input', Input);
+componentMap.set('InputNumber', InputNumber);
+
+componentMap.set('Select', Select);
+componentMap.set('ApiSelect', ApiSelect);
+componentMap.set('Switch', Switch);
+componentMap.set('Checkbox', Checkbox);
+componentMap.set('DatePicker', DatePicker);
+componentMap.set('TimePicker', TimePicker);
+
+export function add(compName: ComponentType, component: Component) {
+  componentMap.set(compName, component);
+}
+
+export function del(compName: ComponentType) {
+  componentMap.delete(compName);
+}
+
+export { componentMap };

+ 20 - 0
src/components/EasyTable/src/components/EditTableHeaderIcon.vue

@@ -0,0 +1,20 @@
+<template>
+  <span>
+    <slot></slot>
+    {{ title }}
+    <FormOutlined />
+  </span>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { FormOutlined } from '@ant-design/icons-vue';
+
+  import { propTypes } from '/@/utils/propTypes';
+  export default defineComponent({
+    name: 'EditTableHeaderIcon',
+    components: { FormOutlined },
+    props: {
+      title: propTypes.string.def(''),
+    },
+  });
+</script>

+ 19 - 0
src/components/EasyTable/src/components/ExpandIcon.tsx

@@ -0,0 +1,19 @@
+import { BasicArrow } from '/@/components/Basic';
+
+export default () => {
+  return (props: Recordable) => {
+    if (!props.expandable) {
+      return <span />;
+    }
+    return (
+      <BasicArrow
+        class="mr-1"
+        iconStyle="margin:0 5px; margin-top: -2px;"
+        onClick={(e: Event) => {
+          props.onExpand(props.record, e);
+        }}
+        expand={props.expanded}
+      />
+    );
+  };
+};

+ 49 - 0
src/components/EasyTable/src/components/HeaderCell.vue

@@ -0,0 +1,49 @@
+<template>
+  <EditTableHeaderCell v-if="getIsEdit">
+    {{ getTitle }}
+  </EditTableHeaderCell>
+  <span v-else>{{ getTitle }}</span>
+  <BasicHelp v-if="getHelpMessage" :text="getHelpMessage" :class="`${prefixCls}__help`" />
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import type { BasicColumn } from '../types/table';
+
+  import { defineComponent, computed } from 'vue';
+  import BasicHelp from '/@/components/Basic/src/BasicHelp.vue';
+  import EditTableHeaderCell from './EditTableHeaderIcon.vue';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+  export default defineComponent({
+    name: 'TableHeaderCell',
+    components: {
+      EditTableHeaderCell,
+      BasicHelp,
+    },
+    props: {
+      column: {
+        type: Object as PropType<BasicColumn>,
+        default: () => ({}),
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('basic-table-header-cell');
+
+      const getIsEdit = computed(() => !!props.column?.edit);
+      const getTitle = computed(() => props.column?.customTitle);
+      const getHelpMessage = computed(() => props.column?.helpMessage);
+
+      return { prefixCls, getIsEdit, getTitle, getHelpMessage };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-header-cell';
+
+  .@{prefix-cls} {
+    &__help {
+      margin-left: 8px;
+      color: rgba(0, 0, 0, 0.65) !important;
+    }
+  }
+</style>

+ 184 - 0
src/components/EasyTable/src/components/TableAction.vue

@@ -0,0 +1,184 @@
+<template>
+  <div :class="[prefixCls, getAlign]" @click="onCellClick">
+    <template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
+      <PopConfirmButton v-bind="action">
+        <Icon :icon="action.icon" class="mr-1" v-if="action.icon" />
+        {{ action.label }}
+      </PopConfirmButton>
+      <Divider
+        type="vertical"
+        class="action-divider"
+        v-if="divider && index < getActions.length - (dropDownActions ? 0 : 1)"
+      />
+    </template>
+    <Dropdown
+      :trigger="['hover']"
+      :dropMenuList="getDropdownList"
+      popconfirm
+      v-if="dropDownActions"
+    >
+      <slot name="more"></slot>
+      <a-button type="link" size="small" v-if="!$slots.more">
+        <MoreOutlined class="icon-more" />
+      </a-button>
+    </Dropdown>
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, PropType, computed, toRaw } from 'vue';
+  import { MoreOutlined } from '@ant-design/icons-vue';
+  import { Divider } from 'ant-design-vue';
+
+  import Icon from '/@/components/Icon/index';
+  import { ActionItem, TableActionType } from '/@/components/Table';
+  import { PopConfirmButton } from '/@/components/Button';
+  import { Dropdown } from '/@/components/Dropdown';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useTableContext } from '../hooks/useTableContext';
+  import { usePermission } from '/@/hooks/web/usePermission';
+
+  import { isBoolean, isFunction } from '/@/utils/is';
+  import { propTypes } from '/@/utils/propTypes';
+  import { ACTION_COLUMN_FLAG } from '../const';
+
+  export default defineComponent({
+    name: 'TableAction',
+    components: { Icon, PopConfirmButton, Divider, Dropdown, MoreOutlined },
+    props: {
+      actions: {
+        type: Array as PropType<ActionItem[]>,
+        default: null,
+      },
+      dropDownActions: {
+        type: Array as PropType<ActionItem[]>,
+        default: null,
+      },
+      divider: propTypes.bool.def(true),
+      outside: propTypes.bool,
+      stopButtonPropagation: propTypes.bool.def(false),
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('basic-table-action');
+      let table: Partial<TableActionType> = {};
+      if (!props.outside) {
+        table = useTableContext();
+      }
+
+      const { hasPermission } = usePermission();
+      function isIfShow(action: ActionItem): boolean {
+        const ifShow = action.ifShow;
+
+        let isIfShow = true;
+
+        if (isBoolean(ifShow)) {
+          isIfShow = ifShow;
+        }
+        if (isFunction(ifShow)) {
+          isIfShow = ifShow(action);
+        }
+        return isIfShow;
+      }
+
+      const getActions = computed(() => {
+        return (toRaw(props.actions) || [])
+          .filter((action) => {
+            return hasPermission(action.auth) && isIfShow(action);
+          })
+          .map((action) => {
+            const { popConfirm } = action;
+            return {
+              type: 'link',
+              size: 'small',
+              ...action,
+              ...(popConfirm || {}),
+              onConfirm: popConfirm?.confirm,
+              onCancel: popConfirm?.cancel,
+              enable: !!popConfirm,
+            };
+          });
+      });
+
+      const getDropdownList = computed(() => {
+        return (toRaw(props.dropDownActions) || [])
+          .filter((action) => {
+            return hasPermission(action.auth) && isIfShow(action);
+          })
+          .map((action, index) => {
+            const { label, popConfirm } = action;
+            return {
+              ...action,
+              ...popConfirm,
+              onConfirm: popConfirm?.confirm,
+              onCancel: popConfirm?.cancel,
+              text: label,
+              divider: index < props.dropDownActions.length - 1 ? props.divider : false,
+            };
+          });
+      });
+
+      const getAlign = computed(() => {
+        const columns = (table as TableActionType)?.getColumns?.() || [];
+        const actionColumn = columns.find((item) => item.flag === ACTION_COLUMN_FLAG);
+        return actionColumn?.align ?? 'left';
+      });
+
+      function onCellClick(e: MouseEvent) {
+        if (!props.stopButtonPropagation) return;
+        const target = e.target as HTMLElement;
+        if (target.tagName === 'BUTTON') {
+          e.stopPropagation();
+        }
+      }
+
+      return { prefixCls, getActions, getDropdownList, getAlign, onCellClick };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-action';
+
+  .@{prefix-cls} {
+    display: flex;
+    align-items: center;
+
+    .action-divider {
+      display: table;
+    }
+
+    &.left {
+      justify-content: flex-start;
+    }
+
+    &.center {
+      justify-content: center;
+    }
+
+    &.right {
+      justify-content: flex-end;
+    }
+
+    button {
+      display: flex;
+      align-items: center;
+
+      span {
+        margin-left: 0 !important;
+      }
+    }
+
+    .ant-divider,
+    .ant-divider-vertical {
+      margin: 0 2px;
+    }
+
+    .icon-more {
+      transform: rotate(90deg);
+
+      svg {
+        font-size: 1.1em;
+        font-weight: 700;
+      }
+    }
+  }
+</style>

+ 95 - 0
src/components/EasyTable/src/components/TableFooter.vue

@@ -0,0 +1,95 @@
+<template>
+  <Table
+    v-if="summaryFunc || summaryData"
+    :showHeader="false"
+    :bordered="false"
+    :pagination="false"
+    :dataSource="getDataSource"
+    :rowKey="(r) => r[rowKey]"
+    :columns="getColumns"
+    tableLayout="fixed"
+    :scroll="scroll"
+  />
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+
+  import { defineComponent, unref, computed, toRaw } from 'vue';
+  import { Table } from 'ant-design-vue';
+  import { cloneDeep } from 'lodash-es';
+  import { isFunction } from '/@/utils/is';
+  import type { BasicColumn } from '../types/table';
+  import { INDEX_COLUMN_FLAG } from '../const';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useTableContext } from '../hooks/useTableContext';
+
+  const SUMMARY_ROW_KEY = '_row';
+  const SUMMARY_INDEX_KEY = '_index';
+  export default defineComponent({
+    name: 'BasicTableFooter',
+    components: { Table },
+    props: {
+      summaryFunc: {
+        type: Function as PropType<Fn>,
+      },
+      summaryData: {
+        type: Array as PropType<Recordable[]>,
+      },
+      scroll: {
+        type: Object as PropType<Recordable>,
+      },
+      rowKey: propTypes.string.def('key'),
+    },
+    setup(props) {
+      const table = useTableContext();
+
+      const getDataSource = computed((): Recordable[] => {
+        const { summaryFunc, summaryData } = props;
+        if (summaryData?.length) {
+          summaryData.forEach((item, i) => (item[props.rowKey] = `${i}`));
+          return summaryData;
+        }
+        if (!isFunction(summaryFunc)) {
+          return [];
+        }
+        let dataSource = toRaw(unref(table.getDataSource()));
+        dataSource = summaryFunc(dataSource);
+        dataSource.forEach((item, i) => {
+          item[props.rowKey] = `${i}`;
+        });
+        return dataSource;
+      });
+
+      const getColumns = computed(() => {
+        const dataSource = unref(getDataSource);
+        const columns: BasicColumn[] = cloneDeep(table.getColumns());
+        const index = columns.findIndex((item) => item.flag === INDEX_COLUMN_FLAG);
+        const hasRowSummary = dataSource.some((item) => Reflect.has(item, SUMMARY_ROW_KEY));
+        const hasIndexSummary = dataSource.some((item) => Reflect.has(item, SUMMARY_INDEX_KEY));
+
+        if (index !== -1) {
+          if (hasIndexSummary) {
+            columns[index].customRender = ({ record }) => record[SUMMARY_INDEX_KEY];
+            columns[index].ellipsis = false;
+          } else {
+            Reflect.deleteProperty(columns[index], 'customRender');
+          }
+        }
+
+        if (table.getRowSelection() && hasRowSummary) {
+          const isFixed = columns.some((col) => col.fixed === 'left');
+          columns.unshift({
+            width: 60,
+            title: 'selection',
+            key: 'selectionKey',
+            align: 'center',
+            ...(isFixed ? { fixed: 'left' } : {}),
+            customRender: ({ record }) => record[SUMMARY_ROW_KEY],
+          });
+        }
+        return columns;
+      });
+      return { getColumns, getDataSource };
+    },
+  });
+</script>

+ 74 - 0
src/components/EasyTable/src/components/TableHeader.vue

@@ -0,0 +1,74 @@
+<template>
+  <slot name="tableTitle" v-if="$slots.tableTitle"></slot>
+
+  <TableTitle :helpMessage="titleHelpMessage" :title="title" v-if="!$slots.tableTitle && title" />
+
+  <div :class="`${prefixCls}__toolbar`">
+    <slot name="toolbar"></slot>
+    <Divider type="vertical" v-if="$slots.toolbar && showTableSetting" />
+    <TableSetting
+      :setting="tableSetting"
+      v-if="showTableSetting"
+      @columns-change="handleColumnChange"
+    />
+  </div>
+</template>
+<script lang="ts">
+  import type { TableSetting, ColumnChangeParam } from '../types/table';
+  import type { PropType } from 'vue';
+
+  import { defineComponent } from 'vue';
+  import { Divider } from 'ant-design-vue';
+  import TableSettingComponent from './settings/index.vue';
+  import TableTitle from './TableTitle.vue';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  export default defineComponent({
+    name: 'BasicTableHeader',
+    components: {
+      Divider,
+      TableTitle,
+      TableSetting: TableSettingComponent,
+    },
+    props: {
+      title: {
+        type: [Function, String] as PropType<string | ((data: Recordable) => string)>,
+      },
+      tableSetting: {
+        type: Object as PropType<TableSetting>,
+      },
+      showTableSetting: {
+        type: Boolean,
+      },
+      titleHelpMessage: {
+        type: [String, Array] as PropType<string | string[]>,
+        default: '',
+      },
+    },
+    emits: ['columns-change'],
+    setup(_, { emit }) {
+      const { prefixCls } = useDesign('basic-table-header');
+      function handleColumnChange(data: ColumnChangeParam[]) {
+        emit('columns-change', data);
+      }
+      return { prefixCls, handleColumnChange };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-header';
+
+  .@{prefix-cls} {
+    &__toolbar {
+      flex: 1;
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+
+      > * {
+        margin-right: 8px;
+      }
+    }
+  }
+</style>

+ 86 - 0
src/components/EasyTable/src/components/TableImg.vue

@@ -0,0 +1,86 @@
+<template>
+  <div
+    :class="prefixCls"
+    class="flex items-center mx-auto"
+    v-if="imgList && imgList.length"
+    :style="getWrapStyle"
+  >
+    <Badge :count="!showBadge || imgList.length == 1 ? 0 : imgList.length" v-if="simpleShow">
+      <div class="img-div">
+        <PreviewGroup>
+          <template v-for="(img, index) in imgList" :key="img">
+            <Image
+              :width="size"
+              :style="{
+                display: index === 0 ? '' : 'none !important',
+              }"
+              :src="srcPrefix + img"
+            />
+          </template>
+        </PreviewGroup>
+      </div>
+    </Badge>
+    <PreviewGroup v-else>
+      <template v-for="(img, index) in imgList" :key="img">
+        <Image
+          :width="size"
+          :style="{ 'margin-left': index === 0 ? 0 : margin }"
+          :src="srcPrefix + img"
+        />
+      </template>
+    </PreviewGroup>
+  </div>
+</template>
+<script lang="ts">
+  import type { CSSProperties } from 'vue';
+  import { defineComponent, computed } from 'vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  import { Image, Badge } from 'ant-design-vue';
+  import { propTypes } from '/@/utils/propTypes';
+
+  export default defineComponent({
+    name: 'TableImage',
+    components: { Image, PreviewGroup: Image.PreviewGroup, Badge },
+    props: {
+      imgList: propTypes.arrayOf(propTypes.string),
+      size: propTypes.number.def(40),
+      // 是否简单显示(只显示第一张图片)
+      simpleShow: propTypes.bool,
+      // 简单模式下是否显示图片数量的badge
+      showBadge: propTypes.bool.def(true),
+      // 图片间距
+      margin: propTypes.number.def(4),
+      // src前缀,将会附加在imgList中每一项之前
+      srcPrefix: propTypes.string.def(''),
+    },
+    setup(props) {
+      const getWrapStyle = computed((): CSSProperties => {
+        const { size } = props;
+        const s = `${size}px`;
+        return { height: s, width: s };
+      });
+
+      const { prefixCls } = useDesign('basic-table-img');
+      return { prefixCls, getWrapStyle };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-img';
+
+  .@{prefix-cls} {
+    .ant-image {
+      margin-right: 4px;
+      cursor: zoom-in;
+
+      img {
+        border-radius: 2px;
+      }
+    }
+
+    .img-div {
+      display: inline-grid;
+    }
+  }
+</style>

+ 53 - 0
src/components/EasyTable/src/components/TableTitle.vue

@@ -0,0 +1,53 @@
+<template>
+  <BasicTitle :class="prefixCls" v-if="getTitle" :helpMessage="helpMessage">
+    {{ getTitle }}
+  </BasicTitle>
+</template>
+<script lang="ts">
+  import { computed, defineComponent, PropType } from 'vue';
+
+  import { BasicTitle } from '/@/components/Basic/index';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { isFunction } from '/@/utils/is';
+  export default defineComponent({
+    name: 'BasicTableTitle',
+    components: { BasicTitle },
+    props: {
+      title: {
+        type: [Function, String] as PropType<string | ((data: Recordable) => string)>,
+      },
+      getSelectRows: {
+        type: Function as PropType<() => Recordable[]>,
+      },
+      helpMessage: {
+        type: [String, Array] as PropType<string | string[]>,
+      },
+    },
+    setup(props) {
+      const { prefixCls } = useDesign('basic-table-title');
+
+      const getTitle = computed(() => {
+        const { title, getSelectRows = () => {} } = props;
+        let tit = title;
+
+        if (isFunction(title)) {
+          tit = title({
+            selectRows: getSelectRows(),
+          });
+        }
+        return tit;
+      });
+
+      return { getTitle, prefixCls };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-table-title';
+
+  .@{prefix-cls} {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+</style>

+ 33 - 0
src/components/EasyTable/src/components/editable/CellComponent.ts

@@ -0,0 +1,33 @@
+import type { FunctionalComponent, defineComponent } from 'vue';
+import type { ComponentType } from '../../types/componentType';
+import { componentMap } from '/@/components/Table/src/componentMap';
+
+import { Popover } from 'ant-design-vue';
+import { h } from 'vue';
+
+export interface ComponentProps {
+  component: ComponentType;
+  rule: boolean;
+  popoverVisible: boolean;
+  ruleMessage: string;
+}
+
+export const CellComponent: FunctionalComponent = (
+  { component = 'Input', rule = true, ruleMessage, popoverVisible }: ComponentProps,
+  { attrs }
+) => {
+  const Comp = componentMap.get(component) as typeof defineComponent;
+
+  const DefaultComp = h(Comp, attrs);
+  if (!rule) {
+    return DefaultComp;
+  }
+  return h(
+    Popover,
+    { overlayClassName: 'edit-cell-rule-popover', visible: !!popoverVisible },
+    {
+      default: () => DefaultComp,
+      content: () => ruleMessage,
+    }
+  );
+};

+ 412 - 0
src/components/EasyTable/src/components/editable/EditableCell.vue

@@ -0,0 +1,412 @@
+<template>
+  <div :class="prefixCls">
+    <div v-show="!isEdit" :class="`${prefixCls}__normal`" @click="handleEdit">
+      {{ getValues || '&nbsp;' }}
+      <FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" />
+    </div>
+
+    <div v-if="isEdit" :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside">
+      <CellComponent
+        v-bind="getComponentProps"
+        :component="getComponent"
+        :style="getWrapperStyle"
+        :popoverVisible="getRuleVisible"
+        :rule="getRule"
+        :ruleMessage="ruleMessage"
+        :class="getWrapperClass"
+        size="small"
+        ref="elRef"
+        @change="handleChange"
+        @options-change="handleOptionsChange"
+        @pressEnter="handleEnter"
+      />
+      <div :class="`${prefixCls}__action`" v-if="!getRowEditable">
+        <CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmit" />
+        <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import type { CSSProperties, PropType } from 'vue';
+  import type { BasicColumn } from '../../types/table';
+  import type { EditRecordRow } from './index';
+
+  import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
+  import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
+  import { CellComponent } from './CellComponent';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useTableContext } from '../../hooks/useTableContext';
+
+  import clickOutside from '/@/directives/clickOutside';
+
+  import { propTypes } from '/@/utils/propTypes';
+  import { isString, isBoolean, isFunction, isNumber, isArray } from '/@/utils/is';
+  import { createPlaceholderMessage } from './helper';
+
+  export default defineComponent({
+    name: 'EditableCell',
+    components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
+    directives: {
+      clickOutside,
+    },
+    props: {
+      value: {
+        type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
+        default: '',
+      },
+      record: {
+        type: Object as PropType<EditRecordRow>,
+      },
+      column: {
+        type: Object as PropType<BasicColumn>,
+        default: () => ({}),
+      },
+      index: propTypes.number,
+    },
+    setup(props) {
+      const table = useTableContext();
+      const isEdit = ref(false);
+      const elRef = ref();
+      const ruleVisible = ref(false);
+      const ruleMessage = ref('');
+      const optionsRef = ref<LabelValueOptions>([]);
+      const currentValueRef = ref<any>(props.value);
+      const defaultValueRef = ref<any>(props.value);
+
+      const { prefixCls } = useDesign('editable-cell');
+
+      const getComponent = computed(() => props.column?.editComponent || 'Input');
+      const getRule = computed(() => props.column?.editRule);
+
+      const getRuleVisible = computed(() => {
+        return unref(ruleMessage) && unref(ruleVisible);
+      });
+
+      const getIsCheckComp = computed(() => {
+        const component = unref(getComponent);
+        return ['Checkbox', 'Switch'].includes(component);
+      });
+
+      const getComponentProps = computed(() => {
+        const compProps = props.column?.editComponentProps ?? {};
+        const component = unref(getComponent);
+        const apiSelectProps: Recordable = {};
+        if (component === 'ApiSelect') {
+          apiSelectProps.cache = true;
+        }
+
+        const isCheckValue = unref(getIsCheckComp);
+
+        const valueField = isCheckValue ? 'checked' : 'value';
+        const val = unref(currentValueRef);
+
+        const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
+
+        return {
+          placeholder: createPlaceholderMessage(unref(getComponent)),
+          ...apiSelectProps,
+          ...compProps,
+          [valueField]: value,
+        };
+      });
+
+      const getValues = computed(() => {
+        const { editComponentProps, editValueMap } = props.column;
+
+        const value = unref(currentValueRef);
+
+        if (editValueMap && isFunction(editValueMap)) {
+          return editValueMap(value);
+        }
+
+        const component = unref(getComponent);
+        if (!component.includes('Select')) {
+          return value;
+        }
+
+        const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
+        const option = options.find((item) => `${item.value}` === `${value}`);
+
+        return option?.label ?? value;
+      });
+
+      const getWrapperStyle = computed((): CSSProperties => {
+        if (unref(getIsCheckComp) || unref(getRowEditable)) {
+          return {};
+        }
+        return {
+          width: 'calc(100% - 48px)',
+        };
+      });
+
+      const getWrapperClass = computed(() => {
+        const { align = 'center' } = props.column;
+        return `edit-cell-align-${align}`;
+      });
+
+      const getRowEditable = computed(() => {
+        const { editable } = props.record || {};
+        return !!editable;
+      });
+
+      watchEffect(() => {
+        defaultValueRef.value = props.value;
+      });
+
+      watchEffect(() => {
+        const { editable } = props.column;
+        if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
+          isEdit.value = !!editable || unref(getRowEditable);
+        }
+      });
+
+      function handleEdit() {
+        if (unref(getRowEditable) || unref(props.column?.editRow)) return;
+        ruleMessage.value = '';
+        isEdit.value = true;
+        nextTick(() => {
+          const el = unref(elRef);
+          el?.focus?.();
+        });
+      }
+
+      async function handleChange(e: any) {
+        const component = unref(getComponent);
+        if (!e) {
+          currentValueRef.value = e;
+        } else if (e?.target && Reflect.has(e.target, 'value')) {
+          currentValueRef.value = (e as ChangeEvent).target.value;
+        } else if (component === 'Checkbox') {
+          currentValueRef.value = (e as ChangeEvent).target.checked;
+        } else if (isString(e) || isBoolean(e) || isNumber(e)) {
+          currentValueRef.value = e;
+        }
+
+        table.emit?.('edit-change', {
+          column: props.column,
+          value: unref(currentValueRef),
+          record: toRaw(props.record),
+        });
+        handleSubmiRule();
+      }
+
+      async function handleSubmiRule() {
+        const { column, record } = props;
+        const { editRule } = column;
+        const currentValue = unref(currentValueRef);
+
+        if (editRule) {
+          if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
+            ruleVisible.value = true;
+            const component = unref(getComponent);
+            const message = createPlaceholderMessage(component);
+            ruleMessage.value = message;
+            return false;
+          }
+          if (isFunction(editRule)) {
+            const res = await editRule(currentValue, record as Recordable);
+            if (!!res) {
+              ruleMessage.value = res;
+              ruleVisible.value = true;
+              return false;
+            } else {
+              ruleMessage.value = '';
+              return true;
+            }
+          }
+        }
+        ruleMessage.value = '';
+        return true;
+      }
+
+      async function handleSubmit(needEmit = true, valid = true) {
+        if (valid) {
+          const isPass = await handleSubmiRule();
+          if (!isPass) return false;
+        }
+
+        const { column, index } = props;
+        const { key, dataIndex } = column;
+        const value = unref(currentValueRef);
+        if (!key || !dataIndex) return;
+
+        const dataKey = (dataIndex || key) as string;
+
+        const record = await table.updateTableData(index, dataKey, value);
+        needEmit && table.emit?.('edit-end', { record, index, key, value });
+        isEdit.value = false;
+      }
+
+      async function handleEnter() {
+        if (props.column?.editRow) {
+          return;
+        }
+        handleSubmit();
+      }
+
+      function handleCancel() {
+        isEdit.value = false;
+        currentValueRef.value = defaultValueRef.value;
+        table.emit?.('edit-cancel', unref(currentValueRef));
+      }
+
+      function onClickOutside() {
+        if (props.column?.editable || unref(getRowEditable)) {
+          return;
+        }
+        const component = unref(getComponent);
+
+        if (component.includes('Input')) {
+          handleCancel();
+        }
+      }
+
+      // only ApiSelect
+      function handleOptionsChange(options: LabelValueOptions) {
+        optionsRef.value = options;
+      }
+
+      function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
+        if (props.record) {
+          /* eslint-disable  */
+          isArray(props.record[cbs])
+            ? props.record[cbs]?.push(handle)
+            : (props.record[cbs] = [handle]);
+        }
+      }
+
+      if (props.record) {
+        initCbs('submitCbs', handleSubmit);
+        initCbs('validCbs', handleSubmiRule);
+        initCbs('cancelCbs', handleCancel);
+
+        if (props.column.dataIndex) {
+          if (!props.record.editValueRefs) props.record.editValueRefs = {};
+          props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
+        }
+        /* eslint-disable  */
+        props.record.onCancelEdit = () => {
+          isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
+        };
+        /* eslint-disable */
+        props.record.onSubmitEdit = async () => {
+          if (isArray(props.record?.submitCbs)) {
+            const validFns = (props.record?.validCbs || []).map((fn) => fn());
+
+            const res = await Promise.all(validFns);
+
+            const pass = res.every((item) => !!item);
+
+            if (!pass) return;
+            const submitFns = props.record?.submitCbs || [];
+            submitFns.forEach((fn) => fn(false, false));
+            table.emit?.('edit-row-end');
+            return true;
+          }
+        };
+      }
+
+      return {
+        isEdit,
+        prefixCls,
+        handleEdit,
+        currentValueRef,
+        handleSubmit,
+        handleChange,
+        handleCancel,
+        elRef,
+        getComponent,
+        getRule,
+        onClickOutside,
+        ruleMessage,
+        getRuleVisible,
+        getComponentProps,
+        handleOptionsChange,
+        getWrapperStyle,
+        getWrapperClass,
+        getRowEditable,
+        getValues,
+        handleEnter,
+        // getSize,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-editable-cell';
+
+  .edit-cell-align-left {
+    text-align: left;
+
+    input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
+      text-align: left;
+    }
+  }
+
+  .edit-cell-align-center {
+    text-align: center;
+
+    input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
+      text-align: center;
+    }
+  }
+
+  .edit-cell-align-right {
+    text-align: right;
+
+    input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
+      text-align: right;
+    }
+  }
+
+  .edit-cell-rule-popover {
+    .ant-popover-inner-content {
+      padding: 4px 8px;
+      color: @error-color;
+      // border: 1px solid @error-color;
+      border-radius: 2px;
+    }
+  }
+  .@{prefix-cls} {
+    position: relative;
+
+    &__wrapper {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      > .ant-select {
+        min-width: calc(100% - 50px);
+      }
+    }
+
+    &__icon {
+      &:hover {
+        transform: scale(1.2);
+
+        svg {
+          color: @primary-color;
+        }
+      }
+    }
+
+    &__normal {
+      &-icon {
+        position: absolute;
+        top: 4px;
+        right: 0;
+        display: none;
+        width: 20px;
+        cursor: pointer;
+      }
+    }
+
+    &:hover {
+      .@{prefix-cls}__normal-icon {
+        display: inline-block;
+      }
+    }
+  }
+</style>

+ 28 - 0
src/components/EasyTable/src/components/editable/helper.ts

@@ -0,0 +1,28 @@
+import { ComponentType } from '../../types/componentType';
+import { useI18n } from '/@/hooks/web/useI18n';
+
+const { t } = useI18n();
+
+/**
+ * @description: 生成placeholder
+ */
+export function createPlaceholderMessage(component: ComponentType) {
+  if (component.includes('Input')) {
+    return t('common.inputText');
+  }
+  if (component.includes('Picker')) {
+    return t('common.chooseText');
+  }
+
+  if (
+    component.includes('Select') ||
+    component.includes('Checkbox') ||
+    component.includes('Radio') ||
+    component.includes('Switch') ||
+    component.includes('DatePicker') ||
+    component.includes('TimePicker')
+  ) {
+    return t('common.chooseText');
+  }
+  return '';
+}

+ 55 - 0
src/components/EasyTable/src/components/editable/index.ts

@@ -0,0 +1,55 @@
+import type { BasicColumn } from '/@/components/Table/src/types/table';
+
+import { h, Ref } from 'vue';
+
+import EditableCell from './EditableCell.vue';
+
+interface Params {
+  text: string;
+  record: Recordable;
+  index: number;
+}
+
+export function renderEditCell(column: BasicColumn) {
+  return ({ text: value, record, index }: Params) => {
+    record.onEdit = async (edit: boolean, submit = false) => {
+      if (!submit) {
+        record.editable = edit;
+      }
+
+      if (!edit && submit) {
+        const res = await record.onSubmitEdit?.();
+        if (res) {
+          record.editable = false;
+          return true;
+        }
+        return false;
+      }
+      // cancel
+      if (!edit && !submit) {
+        record.onCancelEdit?.();
+      }
+      return true;
+    };
+
+    return h(EditableCell, {
+      value,
+      record,
+      column,
+      index,
+    });
+  };
+}
+
+export type EditRecordRow<T = Recordable> = Partial<
+  {
+    onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
+    editable: boolean;
+    onCancel: Fn;
+    onSubmit: Fn;
+    submitCbs: Fn[];
+    cancelCbs: Fn[];
+    validCbs: Fn[];
+    editValueRefs: Recordable<Ref>;
+  } & T
+>;

+ 453 - 0
src/components/EasyTable/src/components/settings/ColumnSetting.vue

@@ -0,0 +1,453 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ t('component.table.settingColumn') }}</span>
+    </template>
+    <Popover
+      placement="bottomLeft"
+      trigger="click"
+      @visibleChange="handleVisibleChange"
+      :overlayClassName="`${prefixCls}__cloumn-list`"
+    >
+      <template #title>
+        <div :class="`${prefixCls}__popover-title`">
+          <Checkbox
+            :indeterminate="indeterminate"
+            v-model:checked="checkAll"
+            @change="onCheckAllChange"
+          >
+            {{ t('component.table.settingColumnShow') }}
+          </Checkbox>
+
+          <Checkbox v-model:checked="checkIndex" @change="handleIndexCheckChange">
+            {{ t('component.table.settingIndexColumnShow') }}
+          </Checkbox>
+
+          <Checkbox
+            v-model:checked="checkSelect"
+            @change="handleSelectCheckChange"
+            :disabled="!defaultRowSelection"
+          >
+            {{ t('component.table.settingSelectColumnShow') }}
+          </Checkbox>
+
+          <a-button size="small" type="link" @click="reset">
+            {{ t('common.resetText') }}
+          </a-button>
+        </div>
+      </template>
+
+      <template #content>
+        <ScrollContainer>
+          <CheckboxGroup v-model:value="checkedList" @change="onChange" ref="columnListRef">
+            <template v-for="item in plainOptions" :key="item.value">
+              <div :class="`${prefixCls}__check-item`">
+                <DragOutlined class="table-coulmn-drag-icon" />
+                <Checkbox :value="item.value">
+                  {{ item.label }}
+                </Checkbox>
+
+                <Tooltip placement="bottomLeft" :mouseLeaveDelay="0.4">
+                  <template #title>
+                    {{ t('component.table.settingFixedLeft') }}
+                  </template>
+                  <Icon
+                    icon="line-md:arrow-align-left"
+                    :class="[
+                      `${prefixCls}__fixed-left`,
+                      {
+                        active: item.fixed === 'left',
+                        disabled: !checkedList.includes(item.value),
+                      },
+                    ]"
+                    @click="handleColumnFixed(item, 'left')"
+                  />
+                </Tooltip>
+                <Divider type="vertical" />
+                <Tooltip placement="bottomLeft" :mouseLeaveDelay="0.4">
+                  <template #title>
+                    {{ t('component.table.settingFixedRight') }}
+                  </template>
+                  <Icon
+                    icon="line-md:arrow-align-left"
+                    :class="[
+                      `${prefixCls}__fixed-right`,
+                      {
+                        active: item.fixed === 'right',
+                        disabled: !checkedList.includes(item.value),
+                      },
+                    ]"
+                    @click="handleColumnFixed(item, 'right')"
+                  />
+                </Tooltip>
+              </div>
+            </template>
+          </CheckboxGroup>
+        </ScrollContainer>
+      </template>
+      <SettingOutlined />
+    </Popover>
+  </Tooltip>
+</template>
+<script lang="ts">
+  import {
+    defineComponent,
+    ref,
+    reactive,
+    toRefs,
+    watchEffect,
+    nextTick,
+    unref,
+    computed,
+  } from 'vue';
+  import { Tooltip, Popover, Checkbox, Divider } from 'ant-design-vue';
+  import { SettingOutlined, DragOutlined } from '@ant-design/icons-vue';
+  import { Icon } from '/@/components/Icon';
+  import { ScrollContainer } from '/@/components/Container';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useTableContext } from '../../hooks/useTableContext';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { useSortable } from '/@/hooks/web/useSortable';
+
+  import { isNullAndUnDef } from '/@/utils/is';
+  import { getPopupContainer } from '/@/utils';
+  import { omit } from 'lodash-es';
+
+  import type { BasicColumn, ColumnChangeParam } from '../../types/table';
+
+  interface State {
+    checkAll: boolean;
+    checkedList: string[];
+    defaultCheckList: string[];
+  }
+
+  interface Options {
+    label: string;
+    value: string;
+    fixed?: boolean | 'left' | 'right';
+  }
+
+  export default defineComponent({
+    name: 'ColumnSetting',
+    components: {
+      SettingOutlined,
+      Popover,
+      Tooltip,
+      Checkbox,
+      CheckboxGroup: Checkbox.Group,
+      DragOutlined,
+      ScrollContainer,
+      Divider,
+      Icon,
+    },
+    emits: ['columns-change'],
+
+    setup(_, { emit }) {
+      const { t } = useI18n();
+      const table = useTableContext();
+
+      const defaultRowSelection = omit(table.getRowSelection(), 'selectedRowKeys');
+      let inited = false;
+
+      const cachePlainOptions = ref<Options[]>([]);
+      const plainOptions = ref<Options[]>([]);
+
+      const plainSortOptions = ref<Options[]>([]);
+
+      const columnListRef = ref<ComponentRef>(null);
+
+      const state = reactive<State>({
+        checkAll: true,
+        checkedList: [],
+        defaultCheckList: [],
+      });
+
+      const checkIndex = ref(false);
+      const checkSelect = ref(false);
+
+      const { prefixCls } = useDesign('basic-column-setting');
+
+      const getValues = computed(() => {
+        return unref(table?.getBindValues) || {};
+      });
+
+      watchEffect(() => {
+        const columns = table.getColumns();
+        if (columns.length) {
+          init();
+        }
+      });
+
+      watchEffect(() => {
+        const values = unref(getValues);
+        checkIndex.value = !!values.showIndexColumn;
+        checkSelect.value = !!values.rowSelection;
+      });
+
+      function getColumns() {
+        const ret: Options[] = [];
+        table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach((item) => {
+          ret.push({
+            label: (item.title as string) || (item.customTitle as string),
+            value: (item.dataIndex || item.title) as string,
+            ...item,
+          });
+        });
+        return ret;
+      }
+
+      function init() {
+        const columns = getColumns();
+
+        const checkList = table
+          .getColumns({ ignoreAction: true })
+          .map((item) => {
+            if (item.defaultHidden) {
+              return '';
+            }
+            return item.dataIndex || item.title;
+          })
+          .filter(Boolean) as string[];
+
+        if (!plainOptions.value.length) {
+          plainOptions.value = columns;
+          plainSortOptions.value = columns;
+          cachePlainOptions.value = columns;
+          state.defaultCheckList = checkList;
+        } else {
+          // const fixedColumns = columns.filter((item) =>
+          //   Reflect.has(item, 'fixed')
+          // ) as BasicColumn[];
+
+          unref(plainOptions).forEach((item: BasicColumn) => {
+            const findItem = columns.find((col: BasicColumn) => col.dataIndex === item.dataIndex);
+            if (findItem) {
+              item.fixed = findItem.fixed;
+            }
+          });
+        }
+        state.checkedList = checkList;
+      }
+
+      // checkAll change
+      function onCheckAllChange(e: ChangeEvent) {
+        const checkList = plainOptions.value.map((item) => item.value);
+        if (e.target.checked) {
+          state.checkedList = checkList;
+          setColumns(checkList);
+        } else {
+          state.checkedList = [];
+          setColumns([]);
+        }
+      }
+
+      const indeterminate = computed(() => {
+        const len = plainOptions.value.length;
+        let checkdedLen = state.checkedList.length;
+        unref(checkIndex) && checkdedLen--;
+        return checkdedLen > 0 && checkdedLen < len;
+      });
+
+      // Trigger when check/uncheck a column
+      function onChange(checkedList: string[]) {
+        const len = plainOptions.value.length;
+        state.checkAll = checkedList.length === len;
+
+        const sortList = unref(plainSortOptions).map((item) => item.value);
+        checkedList.sort((prev, next) => {
+          return sortList.indexOf(prev) - sortList.indexOf(next);
+        });
+        setColumns(checkedList);
+      }
+
+      // reset columns
+      function reset() {
+        state.checkedList = [...state.defaultCheckList];
+        state.checkAll = true;
+        plainOptions.value = unref(cachePlainOptions);
+        plainSortOptions.value = unref(cachePlainOptions);
+        setColumns(table.getCacheColumns());
+      }
+
+      // Open the pop-up window for drag and drop initialization
+      function handleVisibleChange() {
+        if (inited) return;
+        nextTick(() => {
+          const columnListEl = unref(columnListRef);
+          if (!columnListEl) return;
+          const el = columnListEl.$el;
+          if (!el) return;
+          // Drag and drop sort
+          const { initSortable } = useSortable(el, {
+            handle: '.table-coulmn-drag-icon ',
+            onEnd: (evt) => {
+              const { oldIndex, newIndex } = evt;
+              if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
+                return;
+              }
+              // Sort column
+              const columns = getColumns();
+
+              if (oldIndex > newIndex) {
+                columns.splice(newIndex, 0, columns[oldIndex]);
+                columns.splice(oldIndex + 1, 1);
+              } else {
+                columns.splice(newIndex + 1, 0, columns[oldIndex]);
+                columns.splice(oldIndex, 1);
+              }
+
+              plainSortOptions.value = columns;
+              plainOptions.value = columns;
+              setColumns(columns);
+            },
+          });
+          initSortable();
+          inited = true;
+        });
+      }
+
+      // Control whether the serial number column is displayed
+      function handleIndexCheckChange(e: ChangeEvent) {
+        table.setProps({
+          showIndexColumn: e.target.checked,
+        });
+      }
+
+      // Control whether the check box is displayed
+      function handleSelectCheckChange(e: ChangeEvent) {
+        table.setProps({
+          rowSelection: e.target.checked ? defaultRowSelection : undefined,
+        });
+      }
+
+      function handleColumnFixed(item: BasicColumn, fixed?: 'left' | 'right') {
+        if (!state.checkedList.includes(item.dataIndex as string)) return;
+
+        const columns = getColumns() as BasicColumn[];
+        const isFixed = item.fixed === fixed ? false : fixed;
+        const index = columns.findIndex((col) => col.dataIndex === item.dataIndex);
+        if (index !== -1) {
+          columns[index].fixed = isFixed;
+        }
+        item.fixed = isFixed;
+
+        if (isFixed && !item.width) {
+          item.width = 100;
+        }
+        table.setCacheColumnsByField?.(item.dataIndex, { fixed: isFixed });
+        setColumns(columns);
+      }
+
+      function setColumns(columns: BasicColumn[] | string[]) {
+        table.setColumns(columns);
+        const data: ColumnChangeParam[] = unref(plainOptions).map((col) => {
+          const visible =
+            columns.findIndex(
+              (c: BasicColumn | string) =>
+                c === col.value || (typeof c !== 'string' && c.dataIndex === col.value)
+            ) !== -1;
+          return { dataIndex: col.value, fixed: col.fixed, visible };
+        });
+
+        emit('columns-change', data);
+      }
+
+      return {
+        t,
+        ...toRefs(state),
+        indeterminate,
+        onCheckAllChange,
+        onChange,
+        plainOptions,
+        reset,
+        prefixCls,
+        columnListRef,
+        handleVisibleChange,
+        checkIndex,
+        checkSelect,
+        handleIndexCheckChange,
+        handleSelectCheckChange,
+        defaultRowSelection,
+        handleColumnFixed,
+        getPopupContainer,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-basic-column-setting';
+
+  .table-coulmn-drag-icon {
+    margin: 0 5px;
+    cursor: move;
+  }
+
+  .@{prefix-cls} {
+    &__popover-title {
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+
+    &__check-item {
+      display: flex;
+      align-items: center;
+      min-width: 100%;
+      padding: 4px 16px 8px 0;
+
+      .ant-checkbox-wrapper {
+        width: 100%;
+
+        &:hover {
+          color: @primary-color;
+        }
+      }
+    }
+
+    &__fixed-left,
+    &__fixed-right {
+      color: rgba(0, 0, 0, 0.45);
+      cursor: pointer;
+
+      &.active,
+      &:hover {
+        color: @primary-color;
+      }
+
+      &.disabled {
+        color: @disabled-color;
+        cursor: not-allowed;
+      }
+    }
+
+    &__fixed-right {
+      transform: rotate(180deg);
+    }
+
+    &__cloumn-list {
+      svg {
+        width: 1em !important;
+        height: 1em !important;
+      }
+
+      .ant-popover-inner-content {
+        // max-height: 360px;
+        padding-right: 0;
+        padding-left: 0;
+        // overflow: auto;
+      }
+
+      .ant-checkbox-group {
+        width: 100%;
+        min-width: 260px;
+        // flex-wrap: wrap;
+      }
+
+      .scrollbar {
+        height: 220px;
+      }
+    }
+  }
+</style>

+ 40 - 0
src/components/EasyTable/src/components/settings/FullScreenSetting.vue

@@ -0,0 +1,40 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ t('component.table.settingFullScreen') }}</span>
+    </template>
+    <FullscreenOutlined @click="toggle" v-if="!isFullscreen" />
+    <FullscreenExitOutlined @click="toggle" v-else />
+  </Tooltip>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Tooltip } from 'ant-design-vue';
+  import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons-vue';
+
+  import { useFullscreen } from '@vueuse/core';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useTableContext } from '../../hooks/useTableContext';
+
+  export default defineComponent({
+    name: 'FullScreenSetting',
+    components: {
+      FullscreenExitOutlined,
+      FullscreenOutlined,
+      Tooltip,
+    },
+
+    setup() {
+      const table = useTableContext();
+      const { t } = useI18n();
+      const { toggle, isFullscreen } = useFullscreen(table.wrapRef);
+
+      return {
+        toggle,
+        isFullscreen,
+        t,
+      };
+    },
+  });
+</script>

+ 34 - 0
src/components/EasyTable/src/components/settings/RedoSetting.vue

@@ -0,0 +1,34 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ t('common.redo') }}</span>
+    </template>
+    <RedoOutlined @click="redo" />
+  </Tooltip>
+</template>
+<script lang="ts">
+  import { defineComponent } from 'vue';
+  import { Tooltip } from 'ant-design-vue';
+  import { RedoOutlined } from '@ant-design/icons-vue';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useTableContext } from '../../hooks/useTableContext';
+
+  export default defineComponent({
+    name: 'RedoSetting',
+    components: {
+      RedoOutlined,
+      Tooltip,
+    },
+    setup() {
+      const table = useTableContext();
+      const { t } = useI18n();
+
+      function redo() {
+        table.reload();
+      }
+
+      return { redo, t };
+    },
+  });
+</script>

+ 37 - 0
src/components/EasyTable/src/components/settings/ShowFormSearch.vue

@@ -0,0 +1,37 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span v-if="state">{{ t('component.table.showFormSearchConfig') }}</span>
+      <span v-if="!state">{{ t('component.table.hideFormSearchConfig') }}</span>
+    </template>
+    <SearchOutlined @click="toggle" />
+  </Tooltip>
+</template>
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import { Tooltip } from 'ant-design-vue';
+  import { SearchOutlined } from '@ant-design/icons-vue';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useTableContext } from '../../hooks/useTableContext';
+
+  export default defineComponent({
+    name: 'ShowFormSearch',
+    components: {
+      SearchOutlined,
+      Tooltip,
+    },
+    setup() {
+      const state = ref(true);
+      const table = useTableContext();
+      const { t } = useI18n();
+
+      function toggle() {
+        state.value = !state.value;
+        table.showTableSearch();
+      }
+
+      return { toggle, t, state };
+    },
+  });
+</script>

+ 66 - 0
src/components/EasyTable/src/components/settings/SizeSetting.vue

@@ -0,0 +1,66 @@
+<template>
+  <Tooltip placement="top">
+    <template #title>
+      <span>{{ t('component.table.settingDens') }}</span>
+    </template>
+
+    <Dropdown placement="bottomCenter" :trigger="['click']" :getPopupContainer="getPopupContainer">
+      <ColumnHeightOutlined />
+      <template #overlay>
+        <Menu @click="handleTitleClick" selectable v-model:selectedKeys="selectedKeysRef">
+          <MenuItem key="default">
+            <span>{{ t('component.table.settingDensDefault') }}</span>
+          </MenuItem>
+          <MenuItem key="middle">
+            <span>{{ t('component.table.settingDensMiddle') }}</span>
+          </MenuItem>
+          <MenuItem key="small">
+            <span>{{ t('component.table.settingDensSmall') }}</span>
+          </MenuItem>
+        </Menu>
+      </template>
+    </Dropdown>
+  </Tooltip>
+</template>
+<script lang="ts">
+  import type { SizeType } from '../../types/table';
+
+  import { defineComponent, ref } from 'vue';
+  import { Tooltip, Dropdown, Menu } from 'ant-design-vue';
+  import { ColumnHeightOutlined } from '@ant-design/icons-vue';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useTableContext } from '../../hooks/useTableContext';
+  import { getPopupContainer } from '/@/utils';
+
+  export default defineComponent({
+    name: 'SizeSetting',
+    components: {
+      ColumnHeightOutlined,
+      Tooltip,
+      Dropdown,
+      Menu,
+      MenuItem: Menu.Item,
+    },
+    setup() {
+      const table = useTableContext();
+      const { t } = useI18n();
+
+      const selectedKeysRef = ref<SizeType[]>([table.getSize()]);
+
+      function handleTitleClick({ key }: { key: SizeType }) {
+        selectedKeysRef.value = [key];
+        table.setProps({
+          size: key,
+        });
+      }
+
+      return {
+        handleTitleClick,
+        selectedKeysRef,
+        getPopupContainer,
+        t,
+      };
+    },
+  });
+</script>

+ 73 - 0
src/components/EasyTable/src/components/settings/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="table-settings">
+    <ShowFormSearch v-if="getSetting.formSearch" />
+    <RedoSetting v-if="getSetting.redo" />
+    <SizeSetting v-if="getSetting.size" />
+    <ColumnSetting v-if="getSetting.setting" @columns-change="handleColumnChange" />
+    <FullScreenSetting v-if="getSetting.fullScreen" />
+  </div>
+</template>
+<script lang="ts">
+  import type { PropType } from 'vue';
+  import type { TableSetting, ColumnChangeParam } from '../../types/table';
+
+  import { defineComponent, computed } from 'vue';
+
+  import ColumnSetting from './ColumnSetting.vue';
+  import SizeSetting from './SizeSetting.vue';
+  import RedoSetting from './RedoSetting.vue';
+  import FullScreenSetting from './FullScreenSetting.vue';
+  import ShowFormSearch from './ShowFormSearch.vue';
+
+  import { useI18n } from '/@/hooks/web/useI18n';
+
+  export default defineComponent({
+    name: 'TableSetting',
+    components: {
+      ColumnSetting,
+      SizeSetting,
+      RedoSetting,
+      FullScreenSetting,
+      ShowFormSearch,
+    },
+    props: {
+      setting: {
+        type: Object as PropType<TableSetting>,
+        default: () => ({}),
+      },
+    },
+    emits: ['columns-change'],
+    setup(props, { emit }) {
+      const { t } = useI18n();
+
+      const getSetting = computed((): TableSetting => {
+        return {
+          redo: true,
+          size: true,
+          setting: true,
+          fullScreen: false,
+          formSearch: true,
+          ...props.setting,
+        };
+      });
+
+      function handleColumnChange(data: ColumnChangeParam[]) {
+        emit('columns-change', data);
+      }
+
+      return { getSetting, t, handleColumnChange };
+    },
+  });
+</script>
+<style lang="less">
+  .table-settings {
+    & > * {
+      margin-right: 12px;
+    }
+
+    svg {
+      width: 1.3em;
+      height: 1.3em;
+    }
+  }
+</style>

+ 26 - 0
src/components/EasyTable/src/const.ts

@@ -0,0 +1,26 @@
+import componentSetting from '/@/settings/componentSetting';
+
+const { table } = componentSetting;
+
+const { pageSizeOptions, defaultPageSize, fetchSetting, defaultSortFn, defaultFilterFn } = table;
+
+export const ROW_KEY = 'key';
+
+// Optional display number per page;
+export const PAGE_SIZE_OPTIONS = pageSizeOptions;
+
+// Number of items displayed per page
+export const PAGE_SIZE = defaultPageSize;
+
+// Common interface field settings
+export const FETCH_SETTING = fetchSetting;
+
+// Configure general sort function
+export const DEFAULT_SORT_FN = defaultSortFn;
+
+export const DEFAULT_FILTER_FN = defaultFilterFn;
+
+//  Default layout of table cells
+export const DEFAULT_ALIGN = 'center';
+export const INDEX_COLUMN_FLAG = 'INDEX';
+export const ACTION_COLUMN_FLAG = 'ACTION';

+ 332 - 0
src/components/EasyTable/src/hooks/useColumns.ts

@@ -0,0 +1,332 @@
+import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table';
+import type { PaginationProps } from '../types/pagination';
+import type { ComputedRef } from 'vue';
+
+import { unref, Ref, computed, watch, ref, toRaw } from 'vue';
+
+import { renderEditCell } from '../components/editable';
+
+import { usePermission } from '/@/hooks/web/usePermission';
+import { useI18n } from '/@/hooks/web/useI18n';
+
+import { isBoolean, isArray, isString, isObject, isFunction } from '/@/utils/is';
+import { isEqual, cloneDeep } from 'lodash-es';
+import { formatToDate } from '/@/utils/dateUtil';
+
+import { DEFAULT_ALIGN, PAGE_SIZE, INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG } from '../const';
+
+function handleItem(item: BasicColumn, ellipsis: boolean) {
+  const { key, dataIndex, children } = item;
+  item.align = item.align || DEFAULT_ALIGN;
+  if (ellipsis) {
+    if (!key) {
+      item.key = dataIndex;
+    }
+    if (!isBoolean(item.ellipsis)) {
+      Object.assign(item, {
+        ellipsis,
+      });
+    }
+  }
+  if (children && children.length) {
+    handleChildren(children, !!ellipsis);
+  }
+}
+
+function handleChildren(children: BasicColumn[] | undefined, ellipsis: boolean) {
+  if (!children) return;
+  children.forEach((item) => {
+    const { children } = item;
+    handleItem(item, ellipsis);
+    handleChildren(children, ellipsis);
+  });
+}
+
+function handleIndexColumn(
+  propsRef: ComputedRef<BasicTableProps>,
+  getPaginationRef: ComputedRef<boolean | PaginationProps>,
+  columns: BasicColumn[]
+) {
+  const { t } = useI18n();
+
+  const { showIndexColumn, indexColumnProps, isTreeTable } = unref(propsRef);
+
+  let pushIndexColumns = false;
+  if (unref(isTreeTable)) {
+    return;
+  }
+  columns.forEach(() => {
+    const indIndex = columns.findIndex((column) => column.flag === INDEX_COLUMN_FLAG);
+    if (showIndexColumn) {
+      pushIndexColumns = indIndex === -1;
+    } else if (!showIndexColumn && indIndex !== -1) {
+      columns.splice(indIndex, 1);
+    }
+  });
+
+  if (!pushIndexColumns) return;
+
+  const isFixedLeft = columns.some((item) => item.fixed === 'left');
+
+  columns.unshift({
+    flag: INDEX_COLUMN_FLAG,
+    width: 50,
+    title: t('component.table.index'),
+    align: 'center',
+    customRender: ({ index }) => {
+      const getPagination = unref(getPaginationRef);
+      if (isBoolean(getPagination)) {
+        return `${index + 1}`;
+      }
+      const { current = 1, pageSize = PAGE_SIZE } = getPagination;
+      return ((current < 1 ? 1 : current) - 1) * pageSize + index + 1;
+    },
+    ...(isFixedLeft
+      ? {
+          fixed: 'left',
+        }
+      : {}),
+    ...indexColumnProps,
+  });
+}
+
+function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
+  const { actionColumn } = unref(propsRef);
+  if (!actionColumn) return;
+
+  const hasIndex = columns.findIndex((column) => column.flag === ACTION_COLUMN_FLAG);
+  if (hasIndex === -1) {
+    columns.push({
+      ...columns[hasIndex],
+      fixed: 'right',
+      ...actionColumn,
+      flag: ACTION_COLUMN_FLAG,
+    });
+  }
+}
+
+export function useColumns(
+  propsRef: ComputedRef<BasicTableProps>,
+  getPaginationRef: ComputedRef<boolean | PaginationProps>
+) {
+  const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
+  let cacheColumns = unref(propsRef).columns;
+
+  const getColumnsRef = computed(() => {
+    const columns = cloneDeep(unref(columnsRef));
+
+    handleIndexColumn(propsRef, getPaginationRef, columns);
+    handleActionColumn(propsRef, columns);
+    if (!columns) {
+      return [];
+    }
+    const { ellipsis } = unref(propsRef);
+
+    columns.forEach((item) => {
+      const { customRender, slots } = item;
+
+      handleItem(
+        item,
+        Reflect.has(item, 'ellipsis') ? !!item.ellipsis : !!ellipsis && !customRender && !slots
+      );
+    });
+    return columns;
+  });
+
+  function isIfShow(column: BasicColumn): boolean {
+    const ifShow = column.ifShow;
+
+    let isIfShow = true;
+
+    if (isBoolean(ifShow)) {
+      isIfShow = ifShow;
+    }
+    if (isFunction(ifShow)) {
+      isIfShow = ifShow(column);
+    }
+    return isIfShow;
+  }
+  const { hasPermission } = usePermission();
+
+  const getViewColumns = computed(() => {
+    const viewColumns = sortFixedColumn(unref(getColumnsRef));
+
+    const columns = cloneDeep(viewColumns);
+    return columns
+      .filter((column) => {
+        return hasPermission(column.auth) && isIfShow(column);
+      })
+      .map((column) => {
+        const { slots, dataIndex, customRender, format, edit, editRow, flag } = column;
+
+        if (!slots || !slots?.title) {
+          column.slots = { title: `header-${dataIndex}`, ...(slots || {}) };
+          column.customTitle = column.title;
+          Reflect.deleteProperty(column, 'title');
+        }
+        const isDefaultAction = [INDEX_COLUMN_FLAG, ACTION_COLUMN_FLAG].includes(flag!);
+        if (!customRender && format && !edit && !isDefaultAction) {
+          column.customRender = ({ text, record, index }) => {
+            return formatCell(text, format, record, index);
+          };
+        }
+
+        // edit table
+        if ((edit || editRow) && !isDefaultAction) {
+          column.customRender = renderEditCell(column);
+        }
+        return column;
+      });
+  });
+
+  watch(
+    () => unref(propsRef).columns,
+    (columns) => {
+      columnsRef.value = columns;
+      cacheColumns = columns?.filter((item) => !item.flag) ?? [];
+    }
+  );
+
+  function setCacheColumnsByField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
+    if (!dataIndex || !value) {
+      return;
+    }
+    cacheColumns.forEach((item) => {
+      if (item.dataIndex === dataIndex) {
+        Object.assign(item, value);
+        return;
+      }
+    });
+  }
+  /**
+   * set columns
+   * @param columnList key|column
+   */
+  function setColumns(columnList: Partial<BasicColumn>[] | string[]) {
+    const columns = cloneDeep(columnList);
+    if (!isArray(columns)) return;
+
+    if (columns.length <= 0) {
+      columnsRef.value = [];
+      return;
+    }
+
+    const firstColumn = columns[0];
+
+    const cacheKeys = cacheColumns.map((item) => item.dataIndex);
+
+    if (!isString(firstColumn)) {
+      columnsRef.value = columns as BasicColumn[];
+    } else {
+      const columnKeys = columns as string[];
+      const newColumns: BasicColumn[] = [];
+      cacheColumns.forEach((item) => {
+        if (columnKeys.includes(item.dataIndex! || (item.key as string))) {
+          newColumns.push({
+            ...item,
+            defaultHidden: false,
+          });
+        } else {
+          newColumns.push({
+            ...item,
+            defaultHidden: true,
+          });
+        }
+      });
+
+      // Sort according to another array
+      if (!isEqual(cacheKeys, columns)) {
+        newColumns.sort((prev, next) => {
+          return (
+            cacheKeys.indexOf(prev.dataIndex as string) -
+            cacheKeys.indexOf(next.dataIndex as string)
+          );
+        });
+      }
+      columnsRef.value = newColumns;
+    }
+  }
+
+  function getColumns(opt?: GetColumnsParams) {
+    const { ignoreIndex, ignoreAction, sort } = opt || {};
+    let columns = toRaw(unref(getColumnsRef));
+    if (ignoreIndex) {
+      columns = columns.filter((item) => item.flag !== INDEX_COLUMN_FLAG);
+    }
+    if (ignoreAction) {
+      columns = columns.filter((item) => item.flag !== ACTION_COLUMN_FLAG);
+    }
+
+    if (sort) {
+      columns = sortFixedColumn(columns);
+    }
+
+    return columns;
+  }
+  function getCacheColumns() {
+    return cacheColumns;
+  }
+
+  return {
+    getColumnsRef,
+    getCacheColumns,
+    getColumns,
+    setColumns,
+    getViewColumns,
+    setCacheColumnsByField,
+  };
+}
+
+function sortFixedColumn(columns: BasicColumn[]) {
+  const fixedLeftColumns: BasicColumn[] = [];
+  const fixedRightColumns: BasicColumn[] = [];
+  const defColumns: BasicColumn[] = [];
+  for (const column of columns) {
+    if (column.fixed === 'left') {
+      fixedLeftColumns.push(column);
+      continue;
+    }
+    if (column.fixed === 'right') {
+      fixedRightColumns.push(column);
+      continue;
+    }
+    defColumns.push(column);
+  }
+  const resultColumns = [...fixedLeftColumns, ...defColumns, ...fixedRightColumns].filter(
+    (item) => !item.defaultHidden
+  );
+
+  return resultColumns;
+}
+
+// format cell
+export function formatCell(text: string, format: CellFormat, record: Recordable, index: number) {
+  if (!format) {
+    return text;
+  }
+
+  // custom function
+  if (isFunction(format)) {
+    return format(text, record, index);
+  }
+
+  try {
+    // date type
+    const DATE_FORMAT_PREFIX = 'date|';
+    if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX)) {
+      const dateFormat = format.replace(DATE_FORMAT_PREFIX, '');
+
+      if (!dateFormat) {
+        return text;
+      }
+      return formatToDate(text, dateFormat);
+    }
+
+    // enum
+    if (isObject(format) && Reflect.has(format, 'size')) {
+      return format.get(text);
+    }
+  } catch (error) {
+    return text;
+  }
+}

+ 92 - 0
src/components/EasyTable/src/hooks/useCustomRow.ts

@@ -0,0 +1,92 @@
+import type { ComputedRef } from 'vue';
+import type { BasicTableProps } from '../types/table';
+import { unref } from 'vue';
+import { ROW_KEY } from '../const';
+import { isString, isFunction } from '/@/utils/is';
+
+interface Options {
+  setSelectedRowKeys: (keys: string[]) => void;
+  getSelectRowKeys: () => string[];
+  clearSelectedRowKeys: () => void;
+  emit: EmitType;
+  getAutoCreateKey: ComputedRef<boolean | undefined>;
+}
+
+function getKey(
+  record: Recordable,
+  rowKey: string | ((record: Record<string, any>) => string) | undefined,
+  autoCreateKey?: boolean
+) {
+  if (!rowKey || autoCreateKey) {
+    return record[ROW_KEY];
+  }
+  if (isString(rowKey)) {
+    return record[rowKey];
+  }
+  if (isFunction(rowKey)) {
+    return record[rowKey(record)];
+  }
+  return null;
+}
+
+export function useCustomRow(
+  propsRef: ComputedRef<BasicTableProps>,
+  { setSelectedRowKeys, getSelectRowKeys, getAutoCreateKey, clearSelectedRowKeys, emit }: Options
+) {
+  const customRow = (record: Recordable, index: number) => {
+    return {
+      onClick: (e: Event) => {
+        e?.stopPropagation();
+        function handleClick() {
+          const { rowSelection, rowKey, clickToRowSelect } = unref(propsRef);
+          if (!rowSelection || !clickToRowSelect) return;
+          const keys = getSelectRowKeys();
+          const key = getKey(record, rowKey, unref(getAutoCreateKey));
+          if (!key) return;
+
+          const isCheckbox = rowSelection.type === 'checkbox';
+          if (isCheckbox) {
+            if (!keys.includes(key)) {
+              setSelectedRowKeys([...keys, key]);
+              return;
+            }
+            const keyIndex = keys.findIndex((item) => item === key);
+            keys.splice(keyIndex, 1);
+            setSelectedRowKeys(keys);
+            return;
+          }
+
+          const isRadio = rowSelection.type === 'radio';
+          if (isRadio) {
+            if (!keys.includes(key)) {
+              if (keys.length) {
+                clearSelectedRowKeys();
+              }
+              setSelectedRowKeys([key]);
+              return;
+            }
+            clearSelectedRowKeys();
+          }
+        }
+        handleClick();
+        emit('row-click', record, index, e);
+      },
+      onDblclick: (event: Event) => {
+        emit('row-dbClick', record, index, event);
+      },
+      onContextmenu: (event: Event) => {
+        emit('row-contextmenu', record, index, event);
+      },
+      onMouseenter: (event: Event) => {
+        emit('row-mouseenter', record, index, event);
+      },
+      onMouseleave: (event: Event) => {
+        emit('row-mouseleave', record, index, event);
+      },
+    };
+  };
+
+  return {
+    customRow,
+  };
+}

+ 297 - 0
src/components/EasyTable/src/hooks/useDataSource.ts

@@ -0,0 +1,297 @@
+import type { BasicTableProps, FetchParams, SorterResult } from '../types/table';
+import type { PaginationProps } from '../types/pagination';
+
+import {
+  ref,
+  unref,
+  ComputedRef,
+  computed,
+  onMounted,
+  watch,
+  reactive,
+  Ref,
+  watchEffect,
+} from 'vue';
+
+import { useTimeoutFn } from '/@/hooks/core/useTimeout';
+
+import { buildUUID } from '/@/utils/uuid';
+import { isFunction, isBoolean } from '/@/utils/is';
+import { get, cloneDeep } from 'lodash-es';
+
+import { FETCH_SETTING, ROW_KEY, PAGE_SIZE } from '../const';
+
+interface ActionType {
+  getPaginationInfo: ComputedRef<boolean | PaginationProps>;
+  setPagination: (info: Partial<PaginationProps>) => void;
+  setLoading: (loading: boolean) => void;
+  clearSelectedRowKeys: () => void;
+  tableData: Ref<Recordable[]>;
+}
+
+interface SearchState {
+  sortInfo: Recordable;
+  filterInfo: Record<string, string[]>;
+}
+export function useDataSource(
+  propsRef: ComputedRef<BasicTableProps>,
+  { getPaginationInfo, setPagination, setLoading, clearSelectedRowKeys, tableData }: ActionType,
+  emit: EmitType
+) {
+  const searchState = reactive<SearchState>({
+    sortInfo: {},
+    filterInfo: {},
+  });
+  const dataSourceRef = ref<Recordable[]>([]);
+
+  watchEffect(() => {
+    tableData.value = unref(dataSourceRef);
+  });
+
+  watch(
+    () => unref(propsRef).dataSource,
+    () => {
+      const { dataSource, api } = unref(propsRef);
+      !api && dataSource && (dataSourceRef.value = dataSource);
+    },
+    {
+      immediate: true,
+    }
+  );
+
+  function handleTableChange(
+    pagination: PaginationProps,
+    filters: Partial<Recordable<string[]>>,
+    sorter: SorterResult
+  ) {
+    const { clearSelectOnPageChange, sortFn, filterFn } = unref(propsRef);
+    if (clearSelectOnPageChange) {
+      clearSelectedRowKeys();
+    }
+    setPagination(pagination);
+
+    const params: Recordable = {};
+    if (sorter && isFunction(sortFn)) {
+      const sortInfo = sortFn(sorter);
+      searchState.sortInfo = sortInfo;
+      params.sortInfo = sortInfo;
+    }
+
+    if (filters && isFunction(filterFn)) {
+      const filterInfo = filterFn(filters);
+      searchState.filterInfo = filterInfo;
+      params.filterInfo = filterInfo;
+    }
+    fetch(params);
+  }
+
+  function setTableKey(items: any[]) {
+    if (!items || !Array.isArray(items)) return;
+    items.forEach((item) => {
+      if (!item[ROW_KEY]) {
+        item[ROW_KEY] = buildUUID();
+      }
+      if (item.children && item.children.length) {
+        setTableKey(item.children);
+      }
+    });
+  }
+
+  const getAutoCreateKey = computed(() => {
+    return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
+  });
+
+  const getRowKey = computed(() => {
+    const { rowKey } = unref(propsRef);
+    return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
+  });
+
+  const getDataSourceRef = computed(() => {
+    const dataSource = unref(dataSourceRef);
+    if (!dataSource || dataSource.length === 0) {
+      return [];
+    }
+    if (unref(getAutoCreateKey)) {
+      const firstItem = dataSource[0];
+      const lastItem = dataSource[dataSource.length - 1];
+
+      if (firstItem && lastItem) {
+        if (!firstItem[ROW_KEY] || !lastItem[ROW_KEY]) {
+          const data = cloneDeep(unref(dataSourceRef));
+          data.forEach((item) => {
+            if (!item[ROW_KEY]) {
+              item[ROW_KEY] = buildUUID();
+            }
+            if (item.children && item.children.length) {
+              setTableKey(item.children);
+            }
+          });
+          dataSourceRef.value = data;
+        }
+      }
+    }
+    return unref(dataSourceRef);
+  });
+
+  async function updateTableData(index: number, key: string, value: any) {
+    const record = dataSourceRef.value[index];
+    if (record) {
+      dataSourceRef.value[index][key] = value;
+    }
+    return dataSourceRef.value[index];
+  }
+
+  function updateTableDataRecord(
+    rowKey: string | number,
+    record: Recordable
+  ): Recordable | undefined {
+    if (!dataSourceRef.value || dataSourceRef.value.length == 0) return;
+    const rowKeyName = unref(getRowKey);
+    if (!rowKeyName) {
+      return;
+    }
+    const row = dataSourceRef.value.find((r) => {
+      if (typeof rowKeyName === 'function') {
+        return (rowKeyName(r) as string) === rowKey;
+      } else {
+        return Reflect.has(r, rowKeyName) && r[rowKeyName] === rowKey;
+      }
+    });
+    if (row) {
+      for (const field in row) {
+        if (Reflect.has(record, field)) row[field] = record[field];
+      }
+      return row;
+    }
+  }
+
+  async function fetch(opt?: FetchParams) {
+    const { api, searchInfo, fetchSetting, beforeFetch, afterFetch } = unref(propsRef);
+    if (!api || !isFunction(api)) return;
+    try {
+      setLoading(true);
+      const { pageField, sizeField, listField, totalField } = fetchSetting || FETCH_SETTING;
+      let pageParams: Recordable = {};
+
+      const { current = 1, pageSize = PAGE_SIZE } = unref(getPaginationInfo) as PaginationProps;
+
+      if (isBoolean(getPaginationInfo)) {
+        pageParams = {};
+      } else {
+        pageParams[pageField] = (opt && opt.page) || current;
+        pageParams[sizeField] = pageSize;
+      }
+
+      const { sortInfo = {}, filterInfo } = searchState;
+
+      let params: Recordable = {
+        ...pageParams,
+        ...searchInfo,
+        ...(opt?.searchInfo ?? {}),
+        ...sortInfo,
+        ...filterInfo,
+        ...(opt?.sortInfo ?? {}),
+        ...(opt?.filterInfo ?? {}),
+      };
+      if (beforeFetch && isFunction(beforeFetch)) {
+        params = beforeFetch(params) || params;
+      }
+      if (params.order === 'ascend') {
+        params.order = 'asc';
+      }
+      if (params.order === 'descend') {
+        params.order = 'desc';
+      }
+      if (params.field) {
+        params.sort = params.field;
+        delete params.field;
+      }
+
+      const res_data = await api(params);
+
+      // 接口数据处理
+      const res = res_data.list || res_data.tree || res_data.row || res_data.rows;
+
+      const isArrayResult = Array.isArray(res);
+
+      let resultItems: Recordable[] = isArrayResult ? res : get(res, listField);
+      if (!resultItems) {
+        resultItems = res;
+      }
+      const resultTotal: number = isArrayResult ? 0 : get(res, totalField);
+
+      // 假如数据变少,导致总页数变少并小于当前选中页码,通过getPaginationRef获取到的页码是不正确的,需获取正确的页码再次执行
+      if (resultTotal) {
+        const currentTotalPage = Math.ceil(resultTotal / pageSize);
+        if (current > currentTotalPage) {
+          setPagination({
+            current: currentTotalPage,
+          });
+          fetch(opt);
+        }
+      }
+
+      if (afterFetch && isFunction(afterFetch)) {
+        resultItems = afterFetch(resultItems) || resultItems;
+      }
+      dataSourceRef.value = resultItems;
+      let sizeChange = false;
+      if (res_data.count > 10) {
+        sizeChange = true;
+      }
+      setPagination({
+        total: res_data.count || 0,
+        showSizeChanger: sizeChange,
+        pageSizeOptions: ['5', '10', '15', '20', '30', '40', '50'],
+      });
+      if (opt && opt.page) {
+        setPagination({
+          current: opt.page || 1,
+        });
+      }
+      emit('fetch-success', {
+        items: unref(resultItems),
+        total: resultTotal,
+      });
+    } catch (error) {
+      emit('fetch-error', error);
+      dataSourceRef.value = [];
+      setPagination({
+        total: 0,
+      });
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  function setTableData<T = Recordable>(values: T[]) {
+    dataSourceRef.value = values;
+  }
+
+  function getDataSource<T = Recordable>() {
+    return getDataSourceRef.value as T[];
+  }
+
+  async function reload(opt?: FetchParams) {
+    await fetch(opt);
+  }
+
+  onMounted(() => {
+    useTimeoutFn(() => {
+      unref(propsRef).immediate && fetch();
+    }, 16);
+  });
+
+  return {
+    getDataSourceRef,
+    getDataSource,
+    getRowKey,
+    setTableData,
+    getAutoCreateKey,
+    fetch,
+    reload,
+    updateTableData,
+    updateTableDataRecord,
+    handleTableChange,
+  };
+}

+ 21 - 0
src/components/EasyTable/src/hooks/useLoading.ts

@@ -0,0 +1,21 @@
+import { ref, ComputedRef, unref, computed, watch } from 'vue';
+import type { BasicTableProps } from '../types/table';
+
+export function useLoading(props: ComputedRef<BasicTableProps>) {
+  const loadingRef = ref(unref(props).loading);
+
+  watch(
+    () => unref(props).loading,
+    (loading) => {
+      loadingRef.value = loading;
+    }
+  );
+
+  const getLoading = computed(() => unref(loadingRef));
+
+  function setLoading(loading: boolean) {
+    loadingRef.value = loading;
+  }
+
+  return { getLoading, setLoading };
+}

+ 76 - 0
src/components/EasyTable/src/hooks/usePagination.tsx

@@ -0,0 +1,76 @@
+import type { PaginationProps } from '../types/pagination';
+import type { BasicTableProps } from '../types/table';
+
+import { computed, unref, ref, ComputedRef } from 'vue';
+import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
+
+import { isBoolean } from '/@/utils/is';
+
+import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../const';
+import { useI18n } from '/@/hooks/web/useI18n';
+
+interface ItemRender {
+  page: number;
+  type: 'page' | 'prev' | 'next';
+  originalElement: any;
+}
+
+function itemRender({ page, type, originalElement }: ItemRender) {
+  if (type === 'prev') {
+    return page === 0 ? null : <LeftOutlined />;
+  } else if (type === 'next') {
+    return page === 1 ? null : <RightOutlined />;
+  }
+  return originalElement;
+}
+
+export function usePagination(refProps: ComputedRef<BasicTableProps>) {
+  const { t } = useI18n();
+
+  const configRef = ref<PaginationProps>({});
+  const show = ref(true);
+
+  const getPaginationInfo = computed((): PaginationProps | boolean => {
+    const { pagination } = unref(refProps);
+
+    if (!unref(show) || (isBoolean(pagination) && !pagination)) {
+      return false;
+    }
+
+    return {
+      current: 1,
+      pageSize: PAGE_SIZE,
+      size: 'small',
+      defaultPageSize: PAGE_SIZE,
+      showTotal: (total) => t('component.table.total', { total }),
+      showSizeChanger: true,
+      pageSizeOptions: PAGE_SIZE_OPTIONS,
+      itemRender: itemRender,
+      showQuickJumper: true,
+      ...(isBoolean(pagination) ? {} : pagination),
+      ...unref(configRef),
+    };
+  });
+
+  function setPagination(info: Partial<PaginationProps>) {
+    const paginationInfo = unref(getPaginationInfo);
+    configRef.value = {
+      ...(!isBoolean(paginationInfo) ? paginationInfo : {}),
+      ...info,
+    };
+  }
+
+  function getPagination() {
+    return unref(getPaginationInfo);
+  }
+
+  function getShowPagination() {
+    return unref(show);
+  }
+
+  async function setShowPagination(flag: boolean) {
+    show.value = flag;
+  }
+
+  return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
+}

+ 93 - 0
src/components/EasyTable/src/hooks/useRowSelection.ts

@@ -0,0 +1,93 @@
+import type { BasicTableProps, TableRowSelection } from '../types/table';
+
+import { computed, ref, unref, ComputedRef, Ref, toRaw } from 'vue';
+import { ROW_KEY } from '../const';
+
+export function useRowSelection(
+  propsRef: ComputedRef<BasicTableProps>,
+  tableData: Ref<Recordable[]>,
+  emit: EmitType
+) {
+  const selectedRowKeysRef = ref<string[]>([]);
+  const selectedRowRef = ref<Recordable[]>([]);
+
+  const getRowSelectionRef = computed((): TableRowSelection | null => {
+    const { rowSelection } = unref(propsRef);
+    if (!rowSelection) {
+      return null;
+    }
+
+    return {
+      selectedRowKeys: unref(selectedRowKeysRef),
+      hideDefaultSelections: false,
+      onChange: (selectedRowKeys: string[], selectedRows: Recordable[]) => {
+        selectedRowKeysRef.value = selectedRowKeys;
+        selectedRowRef.value = selectedRows;
+        emit('selection-change', {
+          keys: selectedRowKeys,
+          rows: selectedRows,
+        });
+      },
+      ...(rowSelection === undefined ? {} : rowSelection),
+    };
+  });
+
+  const getAutoCreateKey = computed(() => {
+    return unref(propsRef).autoCreateKey && !unref(propsRef).rowKey;
+  });
+
+  const getRowKey = computed(() => {
+    const { rowKey } = unref(propsRef);
+    return unref(getAutoCreateKey) ? ROW_KEY : rowKey;
+  });
+
+  function setSelectedRowKeys(rowKeys: string[]) {
+    selectedRowKeysRef.value = rowKeys;
+
+    const rows = toRaw(unref(tableData)).filter((item) =>
+      rowKeys.includes(item[unref(getRowKey) as string])
+    );
+    selectedRowRef.value = rows;
+  }
+
+  function setSelectedRows(rows: Recordable[]) {
+    selectedRowRef.value = rows;
+  }
+
+  function clearSelectedRowKeys() {
+    selectedRowRef.value = [];
+    selectedRowKeysRef.value = [];
+  }
+
+  function deleteSelectRowByKey(key: string) {
+    const selectedRowKeys = unref(selectedRowKeysRef);
+    const index = selectedRowKeys.findIndex((item) => item === key);
+    if (index !== -1) {
+      unref(selectedRowKeysRef).splice(index, 1);
+    }
+  }
+
+  function getSelectRowKeys() {
+    return unref(selectedRowKeysRef);
+  }
+
+  function getSelectRows<T = Recordable>() {
+    // const ret = toRaw(unref(selectedRowRef)).map((item) => toRaw(item));
+    return unref(selectedRowRef) as T[];
+  }
+
+  function getRowSelection() {
+    return unref(getRowSelectionRef)!;
+  }
+
+  return {
+    getRowSelection,
+    getRowSelectionRef,
+    getSelectRows,
+    getSelectRowKeys,
+    setSelectedRowKeys,
+    clearSelectedRowKeys,
+    deleteSelectRowByKey,
+    setSelectedRows,
+  };
+}

+ 144 - 0
src/components/EasyTable/src/hooks/useTable.ts

@@ -0,0 +1,144 @@
+import type { BasicTableProps, TableActionType, BasicColumn } from '../types/table';
+import type { PaginationProps } from '../types/pagination';
+import type { DynamicProps } from '/#/utils';
+import type { FormActionType } from '/@/components/Form';
+import type { WatchStopHandle } from 'vue';
+
+import { getDynamicProps } from '/@/utils';
+import { ref, onUnmounted, unref, watch, toRaw } from 'vue';
+import { isProdMode } from '/@/utils/env';
+import { error } from '/@/utils/log';
+
+type Props = Partial<DynamicProps<BasicTableProps>>;
+
+type UseTableMethod = TableActionType & {
+  getForm: () => FormActionType;
+};
+
+export function useTable(tableProps?: Props): [
+  (instance: TableActionType, formInstance: UseTableMethod) => void,
+  TableActionType & {
+    getForm: () => FormActionType;
+  }
+] {
+  const tableRef = ref<Nullable<TableActionType>>(null);
+  const loadedRef = ref<Nullable<boolean>>(false);
+  const formRef = ref<Nullable<UseTableMethod>>(null);
+
+  let stopWatch: WatchStopHandle;
+
+  function register(instance: TableActionType, formInstance: UseTableMethod) {
+    isProdMode() &&
+      onUnmounted(() => {
+        tableRef.value = null;
+        loadedRef.value = null;
+      });
+
+    if (unref(loadedRef) && isProdMode() && instance === unref(tableRef)) return;
+
+    tableRef.value = instance;
+    formRef.value = formInstance;
+    tableProps && instance.setProps(getDynamicProps(tableProps));
+    loadedRef.value = true;
+
+    stopWatch?.();
+
+    stopWatch = watch(
+      () => tableProps,
+      () => {
+        tableProps && instance.setProps(getDynamicProps(tableProps));
+      },
+      {
+        immediate: true,
+        deep: true,
+      }
+    );
+  }
+
+  function getTableInstance(): TableActionType {
+    const table = unref(tableRef);
+    if (!table) {
+      error(
+        'The table instance has not been obtained yet, please make sure the table is presented when performing the table operation!'
+      );
+    }
+    return table as TableActionType;
+  }
+
+  const methods: TableActionType & {
+    getForm: () => FormActionType;
+  } = {
+    setProps: (props: Partial<BasicTableProps>) => {
+      getTableInstance().setProps(props);
+    },
+    redoHeight: () => {
+      getTableInstance().redoHeight();
+    },
+    setLoading: (loading: boolean) => {
+      getTableInstance().setLoading(loading);
+    },
+    getDataSource: () => {
+      return getTableInstance().getDataSource();
+    },
+    getColumns: ({ ignoreIndex = false }: { ignoreIndex?: boolean } = {}) => {
+      const columns = getTableInstance().getColumns({ ignoreIndex }) || [];
+      return toRaw(columns);
+    },
+    setColumns: (columns: BasicColumn[]) => {
+      getTableInstance().setColumns(columns);
+    },
+    setTableData: (values: any[]) => {
+      return getTableInstance().setTableData(values);
+    },
+    setPagination: (info: Partial<PaginationProps>) => {
+      return getTableInstance().setPagination(info);
+    },
+    deleteSelectRowByKey: (key: string) => {
+      getTableInstance().deleteSelectRowByKey(key);
+    },
+    getSelectRowKeys: () => {
+      return toRaw(getTableInstance().getSelectRowKeys());
+    },
+    getSelectRows: () => {
+      return toRaw(getTableInstance().getSelectRows());
+    },
+    clearSelectedRowKeys: () => {
+      getTableInstance().clearSelectedRowKeys();
+    },
+    setSelectedRowKeys: (keys: string[] | number[]) => {
+      getTableInstance().setSelectedRowKeys(keys);
+    },
+    getPaginationRef: () => {
+      return getTableInstance().getPaginationRef();
+    },
+    getSize: () => {
+      return toRaw(getTableInstance().getSize());
+    },
+    updateTableData: (index: number, key: string, value: any) => {
+      return getTableInstance().updateTableData(index, key, value);
+    },
+    updateTableDataRecord: (rowKey: string | number, record: Recordable) => {
+      return getTableInstance().updateTableDataRecord(rowKey, record);
+    },
+    getRowSelection: () => {
+      return toRaw(getTableInstance().getRowSelection());
+    },
+    getCacheColumns: () => {
+      return toRaw(getTableInstance().getCacheColumns());
+    },
+    getForm: () => {
+      return unref(formRef) as unknown as FormActionType;
+    },
+    setShowPagination: async (show: boolean) => {
+      getTableInstance().setShowPagination(show);
+    },
+    getShowPagination: () => {
+      return toRaw(getTableInstance().getShowPagination());
+    },
+    showTableSearch: () => {
+      getTableInstance().showTableSearch();
+    },
+  };
+
+  return [register, methods];
+}

+ 23 - 0
src/components/EasyTable/src/hooks/useTableContext.ts

@@ -0,0 +1,23 @@
+import type { Ref } from 'vue';
+import type { BasicTableProps, TableActionType } from '../types/table';
+
+import { provide, inject, ComputedRef } from 'vue';
+
+const key = Symbol('basic-table');
+
+type Instance = TableActionType & {
+  wrapRef: Ref<Nullable<HTMLElement>>;
+  getBindValues: ComputedRef<Recordable>;
+};
+
+type RetInstance = Omit<Instance, 'getBindValues'> & {
+  getBindValues: ComputedRef<BasicTableProps>;
+};
+
+export function createTableContext(instance: Instance) {
+  provide(key, instance);
+}
+
+export function useTableContext(): RetInstance {
+  return inject(key) as RetInstance;
+}

+ 57 - 0
src/components/EasyTable/src/hooks/useTableFooter.ts

@@ -0,0 +1,57 @@
+import type { ComputedRef, Ref } from 'vue';
+import type { BasicTableProps } from '../types/table';
+import { unref, computed, h, nextTick, watchEffect } from 'vue';
+import TableFooter from '../components/TableFooter.vue';
+import { useEventListener } from '/@/hooks/event/useEventListener';
+
+export function useTableFooter(
+  propsRef: ComputedRef<BasicTableProps>,
+  scrollRef: ComputedRef<{
+    x: string | number | true;
+    y: Nullable<number>;
+    scrollToFirstRowOnChange: boolean;
+  }>,
+  tableElRef: Ref<ComponentRef>,
+  getDataSourceRef: ComputedRef<Recordable>
+) {
+  const getIsEmptyData = computed(() => {
+    return (unref(getDataSourceRef) || []).length === 0;
+  });
+
+  const getFooterProps = computed((): Recordable | undefined => {
+    const { summaryFunc, showSummary, summaryData } = unref(propsRef);
+    return showSummary && !unref(getIsEmptyData)
+      ? () => h(TableFooter, { summaryFunc, summaryData, scroll: unref(scrollRef) })
+      : undefined;
+  });
+
+  watchEffect(() => {
+    handleSummary();
+  });
+
+  function handleSummary() {
+    const { showSummary } = unref(propsRef);
+    if (!showSummary || unref(getIsEmptyData)) return;
+
+    nextTick(() => {
+      const tableEl = unref(tableElRef);
+      if (!tableEl) return;
+      const bodyDomList = tableEl.$el.querySelectorAll('.ant-table-body');
+      const bodyDom = bodyDomList[0];
+      useEventListener({
+        el: bodyDom,
+        name: 'scroll',
+        listener: () => {
+          const footerBodyDom = tableEl.$el.querySelector(
+            '.ant-table-footer .ant-table-body'
+          ) as HTMLDivElement;
+          if (!footerBodyDom || !bodyDom) return;
+          footerBodyDom.scrollLeft = bodyDom.scrollLeft;
+        },
+        wait: 0,
+        options: true,
+      });
+    });
+  }
+  return { getFooterProps };
+}

+ 51 - 0
src/components/EasyTable/src/hooks/useTableHeader.ts

@@ -0,0 +1,51 @@
+import type { ComputedRef, Slots } from 'vue';
+import type { BasicTableProps, InnerHandlers } from '../types/table';
+
+import { unref, computed, h } from 'vue';
+import TableHeader from '../components/TableHeader.vue';
+
+import { isString } from '/@/utils/is';
+import { getSlot } from '/@/utils/helper/tsxHelper';
+
+export function useTableHeader(
+  propsRef: ComputedRef<BasicTableProps>,
+  slots: Slots,
+  handlers: InnerHandlers
+) {
+  const getHeaderProps = computed((): Recordable => {
+    const { title, showTableSetting, titleHelpMessage, tableSetting } = unref(propsRef);
+    const hideTitle = !slots.tableTitle && !title && !slots.toolbar && !showTableSetting;
+    if (hideTitle && !isString(title)) {
+      return {};
+    }
+
+    return {
+      title: hideTitle
+        ? null
+        : () =>
+            h(
+              TableHeader,
+              {
+                title,
+                titleHelpMessage,
+                showTableSetting,
+                tableSetting,
+                onColumnsChange: handlers.onColumnsChange,
+              } as Recordable,
+              {
+                ...(slots.toolbar
+                  ? {
+                      toolbar: () => getSlot(slots, 'toolbar'),
+                    }
+                  : {}),
+                ...(slots.tableTitle
+                  ? {
+                      tableTitle: () => getSlot(slots, 'tableTitle'),
+                    }
+                  : {}),
+              }
+            ),
+    };
+  });
+  return { getHeaderProps };
+}

+ 178 - 0
src/components/EasyTable/src/hooks/useTableScroll.ts

@@ -0,0 +1,178 @@
+import type { BasicTableProps, TableRowSelection, BasicColumn } from '../types/table';
+import type { Ref, ComputedRef } from 'vue';
+
+import { computed, unref, ref, nextTick, watch } from 'vue';
+
+import { getViewportOffset } from '/@/utils/domUtils';
+import { isBoolean } from '/@/utils/is';
+
+import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
+import { useModalContext } from '/@/components/Modal';
+import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
+import { useDebounceFn } from '@vueuse/core';
+
+export function useTableScroll(
+  propsRef: ComputedRef<BasicTableProps>,
+  tableElRef: Ref<ComponentRef>,
+  columnsRef: ComputedRef<BasicColumn[]>,
+  rowSelectionRef: ComputedRef<TableRowSelection<any> | null>,
+  getDataSourceRef: ComputedRef<Recordable[]>
+) {
+  const tableHeightRef: Ref<Nullable<number>> = ref(null);
+
+  const modalFn = useModalContext();
+
+  // Greater than animation time 280
+  const debounceRedoHeight = useDebounceFn(redoHeight, 100);
+
+  const getCanResize = computed(() => {
+    const { canResize, scroll } = unref(propsRef);
+    return canResize && !(scroll || {}).y;
+  });
+
+  watch(
+    () => [unref(getCanResize), unref(getDataSourceRef)?.length],
+    () => {
+      debounceRedoHeight();
+    },
+    {
+      flush: 'post',
+    }
+  );
+
+  function redoHeight() {
+    nextTick(() => {
+      calcTableHeight();
+    });
+  }
+
+  function setHeight(heigh: number) {
+    tableHeightRef.value = heigh;
+    //  Solve the problem of modal adaptive height calculation when the form is placed in the modal
+    modalFn?.redoModalHeight?.();
+  }
+
+  // No need to repeat queries
+  let paginationEl: HTMLElement | null;
+  let footerEl: HTMLElement | null;
+  let bodyEl: HTMLElement | null;
+
+  async function calcTableHeight() {
+    const { resizeHeightOffset, pagination, maxHeight } = unref(propsRef);
+    const tableData = unref(getDataSourceRef);
+
+    const table = unref(tableElRef);
+    if (!table) return;
+
+    const tableEl: Element = table.$el;
+    if (!tableEl) return;
+
+    if (!bodyEl) {
+      bodyEl = tableEl.querySelector('.ant-table-body');
+    }
+
+    bodyEl!.style.height = 'unset';
+
+    if (!unref(getCanResize) || tableData.length === 0) return;
+
+    await nextTick();
+    //Add a delay to get the correct bottomIncludeBody paginationHeight footerHeight headerHeight
+
+    const headEl = tableEl.querySelector('.ant-table-thead ');
+
+    if (!headEl) return;
+
+    // Table height from bottom
+    const { bottomIncludeBody } = getViewportOffset(headEl);
+    // Table height from bottom height-custom offset
+
+    const paddingHeight = 32;
+    // Pager height
+    let paginationHeight = 2;
+    if (!isBoolean(pagination)) {
+      paginationEl = tableEl.querySelector('.ant-pagination') as HTMLElement;
+      if (paginationEl) {
+        const offsetHeight = paginationEl.offsetHeight;
+        paginationHeight += offsetHeight || 0;
+      } else {
+        // TODO First fix 24
+        paginationHeight += 24;
+      }
+    } else {
+      paginationHeight = -8;
+    }
+
+    let footerHeight = 0;
+    if (!isBoolean(pagination)) {
+      if (!footerEl) {
+        footerEl = tableEl.querySelector('.ant-table-footer') as HTMLElement;
+      } else {
+        const offsetHeight = footerEl.offsetHeight;
+        footerHeight += offsetHeight || 0;
+      }
+    }
+
+    let headerHeight = 0;
+    if (headEl) {
+      headerHeight = (headEl as HTMLElement).offsetHeight;
+    }
+
+    let height =
+      bottomIncludeBody -
+      (resizeHeightOffset || 0) -
+      paddingHeight -
+      paginationHeight -
+      footerHeight -
+      headerHeight;
+
+    height = (height > maxHeight! ? (maxHeight as number) : height) ?? height;
+    setHeight(height);
+
+    bodyEl!.style.height = `${height}px`;
+  }
+  useWindowSizeFn(calcTableHeight, 280);
+  onMountedOrActivated(() => {
+    calcTableHeight();
+    nextTick(() => {
+      debounceRedoHeight();
+    });
+  });
+
+  const getScrollX = computed(() => {
+    let width = 0;
+    if (unref(rowSelectionRef)) {
+      width += 60;
+    }
+
+    // TODO props ?? 0;
+    const NORMAL_WIDTH = 150;
+
+    const columns = unref(columnsRef).filter((item) => !item.defaultHidden);
+    columns.forEach((item) => {
+      width += Number.parseInt(item.width as string) || 0;
+    });
+    const unsetWidthColumns = columns.filter((item) => !Reflect.has(item, 'width'));
+
+    const len = unsetWidthColumns.length;
+    if (len !== 0) {
+      width += len * NORMAL_WIDTH;
+    }
+
+    const table = unref(tableElRef);
+    const tableWidth = table?.$el?.offsetWidth ?? 0;
+    return tableWidth > width ? '100%' : width;
+  });
+
+  const getScrollRef = computed(() => {
+    const tableHeight = unref(tableHeightRef);
+    const { canResize, scroll } = unref(propsRef);
+    return {
+      x: unref(getScrollX),
+      y: canResize ? tableHeight : null,
+      scrollToFirstRowOnChange: false,
+      ...scroll,
+    };
+  });
+
+  return { getScrollRef, redoHeight };
+}

+ 19 - 0
src/components/EasyTable/src/hooks/useTableStyle.ts

@@ -0,0 +1,19 @@
+import type { ComputedRef } from 'vue';
+import type { BasicTableProps, TableCustomRecord } from '../types/table';
+
+import { unref } from 'vue';
+import { isFunction } from '/@/utils/is';
+export function useTableStyle(propsRef: ComputedRef<BasicTableProps>, prefixCls: string) {
+  function getRowClassName(record: TableCustomRecord, index: number) {
+    const { striped, rowClassName } = unref(propsRef);
+    if (!striped) return;
+    if (rowClassName && isFunction(rowClassName)) {
+      return rowClassName(record);
+    }
+    return (index || 0) % 2 === 1 ? `${prefixCls}-row__striped` : '';
+  }
+
+  return {
+    getRowClassName,
+  };
+}

+ 130 - 0
src/components/EasyTable/src/props.ts

@@ -0,0 +1,130 @@
+import type { PropType } from 'vue';
+import type { PaginationProps } from './types/pagination';
+import type {
+  BasicColumn,
+  FetchSetting,
+  TableSetting,
+  SorterResult,
+  TableCustomRecord,
+  TableRowSelection,
+} from './types/table';
+import type { FormProps } from '/@/components/Form';
+import { DEFAULT_FILTER_FN, DEFAULT_SORT_FN, FETCH_SETTING } from './const';
+import { propTypes } from '/@/utils/propTypes';
+
+export const basicProps = {
+  clickToRowSelect: propTypes.bool.def(true),
+  isTreeTable: propTypes.bool.def(false),
+  tableSetting: propTypes.shape<TableSetting>({}),
+  inset: propTypes.bool,
+  sortFn: {
+    type: Function as PropType<(sortInfo: SorterResult) => any>,
+    default: DEFAULT_SORT_FN,
+  },
+  filterFn: {
+    type: Function as PropType<(data: Partial<Recordable<string[]>>) => any>,
+    default: DEFAULT_FILTER_FN,
+  },
+  showTableSetting: propTypes.bool,
+  autoCreateKey: propTypes.bool.def(true),
+  striped: propTypes.bool.def(true),
+  showSummary: propTypes.bool,
+  summaryFunc: {
+    type: [Function, Array] as PropType<(...arg: any[]) => any[]>,
+    default: null,
+  },
+  summaryData: {
+    type: Array as PropType<Recordable[]>,
+    default: null,
+  },
+  indentSize: propTypes.number.def(24),
+  canColDrag: propTypes.bool.def(true),
+  api: {
+    type: Function as PropType<(...arg: any[]) => Promise<any>>,
+    default: null,
+  },
+  beforeFetch: {
+    type: Function as PropType<Fn>,
+    default: null,
+  },
+  afterFetch: {
+    type: Function as PropType<Fn>,
+    default: null,
+  },
+  handleSearchInfoFn: {
+    type: Function as PropType<Fn>,
+    default: null,
+  },
+  fetchSetting: {
+    type: Object as PropType<FetchSetting>,
+    default: () => {
+      return FETCH_SETTING;
+    },
+  },
+  // 立即请求接口
+  immediate: propTypes.bool.def(true),
+  emptyDataIsShowTable: propTypes.bool.def(true),
+  // 额外的请求参数
+  searchInfo: {
+    type: Object as PropType<Recordable>,
+    default: null,
+  },
+  // 使用搜索表单
+  useSearchForm: propTypes.bool,
+  // 表单配置
+  formConfig: {
+    type: Object as PropType<Partial<FormProps>>,
+    default: null,
+  },
+  columns: {
+    type: [Array] as PropType<BasicColumn[]>,
+    default: () => [],
+  },
+  showIndexColumn: propTypes.bool.def(true),
+  indexColumnProps: {
+    type: Object as PropType<BasicColumn>,
+    default: null,
+  },
+  actionColumn: {
+    type: Object as PropType<BasicColumn>,
+    default: null,
+  },
+  ellipsis: propTypes.bool.def(true),
+  canResize: propTypes.bool.def(true),
+  clearSelectOnPageChange: propTypes.bool,
+  resizeHeightOffset: propTypes.number.def(0),
+  rowSelection: {
+    type: Object as PropType<TableRowSelection | null>,
+    default: null,
+  },
+  title: {
+    type: [String, Function] as PropType<string | ((data: Recordable) => string)>,
+    default: null,
+  },
+  titleHelpMessage: {
+    type: [String, Array] as PropType<string | string[]>,
+  },
+  maxHeight: propTypes.number,
+  dataSource: {
+    type: Array as PropType<Recordable[]>,
+    default: null,
+  },
+  rowKey: {
+    type: [String, Function] as PropType<string | ((record: Recordable) => string)>,
+    default: '',
+  },
+  bordered: propTypes.bool,
+  canDragRow: propTypes.bool,
+  pagination: {
+    type: [Object, Boolean] as PropType<PaginationProps | boolean>,
+    default: null,
+  },
+  loading: propTypes.bool,
+  rowClassName: {
+    type: Function as PropType<(record: TableCustomRecord<any>, index: number) => string>,
+  },
+  scroll: {
+    type: Object as PropType<{ x: number | true; y: number }>,
+    default: null,
+  },
+};

+ 198 - 0
src/components/EasyTable/src/types/column.ts

@@ -0,0 +1,198 @@
+import { VNodeChild } from 'vue';
+
+export interface ColumnFilterItem {
+  text?: string;
+  value?: string;
+  children?: any;
+}
+
+export declare type SortOrder = 'ascend' | 'descend';
+
+export interface RecordProps<T> {
+  text: any;
+  record: T;
+  index: number;
+}
+
+export interface FilterDropdownProps {
+  prefixCls?: string;
+  setSelectedKeys?: (selectedKeys: string[]) => void;
+  selectedKeys?: string[];
+  confirm?: () => void;
+  clearFilters?: () => void;
+  filters?: ColumnFilterItem[];
+  getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
+  visible?: boolean;
+}
+
+export declare type CustomRenderFunction<T> = (record: RecordProps<T>) => VNodeChild | JSX.Element;
+
+export interface ColumnProps<T> {
+  /**
+   * specify how content is aligned
+   * @default 'left'
+   * @type string
+   */
+  align?: 'left' | 'right' | 'center';
+
+  /**
+   * ellipsize cell content, not working with sorter and filters for now.
+   * tableLayout would be fixed when ellipsis is true.
+   * @default false
+   * @type boolean
+   */
+  ellipsis?: boolean;
+
+  /**
+   * Span of this column's title
+   * @type number
+   */
+  colSpan?: number;
+
+  /**
+   * Display field of the data record, could be set like a.b.c
+   * @type string
+   */
+  dataIndex?: string;
+
+  /**
+   * Default filtered values
+   * @type string[]
+   */
+  defaultFilteredValue?: string[];
+
+  /**
+   * Default order of sorted values: 'ascend' 'descend' null
+   * @type string
+   */
+  defaultSortOrder?: SortOrder;
+
+  /**
+   * Customized filter overlay
+   * @type any (slot)
+   */
+  filterDropdown?:
+    | VNodeChild
+    | JSX.Element
+    | ((props: FilterDropdownProps) => VNodeChild | JSX.Element);
+
+  /**
+   * Whether filterDropdown is visible
+   * @type boolean
+   */
+  filterDropdownVisible?: boolean;
+
+  /**
+   * Whether the dataSource is filtered
+   * @default false
+   * @type boolean
+   */
+  filtered?: boolean;
+
+  /**
+   * Controlled filtered value, filter icon will highlight
+   * @type string[]
+   */
+  filteredValue?: string[];
+
+  /**
+   * Customized filter icon
+   * @default false
+   * @type any
+   */
+  filterIcon?: boolean | VNodeChild | JSX.Element;
+
+  /**
+   * Whether multiple filters can be selected
+   * @default true
+   * @type boolean
+   */
+  filterMultiple?: boolean;
+
+  /**
+   * Filter menu config
+   * @type object[]
+   */
+  filters?: ColumnFilterItem[];
+
+  /**
+   * Set column to be fixed: true(same as left) 'left' 'right'
+   * @default false
+   * @type boolean | string
+   */
+  fixed?: boolean | 'left' | 'right';
+
+  /**
+   * Unique key of this column, you can ignore this prop if you've set a unique dataIndex
+   * @type string
+   */
+  key?: string;
+
+  /**
+   * Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config
+   * @type Function | ScopedSlot
+   */
+  customRender?: CustomRenderFunction<T> | VNodeChild | JSX.Element;
+
+  /**
+   * Sort function for local sort, see Array.sort's compareFunction. If you need sort buttons only, set to true
+   * @type boolean | Function
+   */
+  sorter?: boolean | Function;
+
+  /**
+   * Order of sorted values: 'ascend' 'descend' false
+   * @type boolean | string
+   */
+  sortOrder?: boolean | SortOrder;
+
+  /**
+   * supported sort way, could be 'ascend', 'descend'
+   * @default ['ascend', 'descend']
+   * @type string[]
+   */
+  sortDirections?: SortOrder[];
+
+  /**
+   * Title of this column
+   * @type any (string | slot)
+   */
+  title?: VNodeChild | JSX.Element;
+
+  /**
+   * Width of this column
+   * @type string | number
+   */
+  width?: string | number;
+
+  /**
+   * Set props on per cell
+   * @type Function
+   */
+  customCell?: (record: T, rowIndex: number) => object;
+
+  /**
+   * Set props on per header cell
+   * @type object
+   */
+  customHeaderCell?: (column: ColumnProps<T>) => object;
+
+  /**
+   * Callback executed when the confirm filter button is clicked, Use as a filter event when using template or jsx
+   * @type Function
+   */
+  onFilter?: (value: any, record: T) => boolean;
+
+  /**
+   * Callback executed when filterDropdownVisible is changed, Use as a filterDropdownVisible event when using template or jsx
+   * @type Function
+   */
+  onFilterDropdownVisibleChange?: (visible: boolean) => void;
+
+  /**
+   * When using columns, you can setting this property to configure the properties that support the slot,
+   * such as slots: { filterIcon: 'XXX'}
+   * @type object
+   */
+  slots?: Recordable<string>;
+}

+ 9 - 0
src/components/EasyTable/src/types/componentType.ts

@@ -0,0 +1,9 @@
+export type ComponentType =
+  | 'Input'
+  | 'InputNumber'
+  | 'Select'
+  | 'ApiSelect'
+  | 'Checkbox'
+  | 'Switch'
+  | 'DatePicker'
+  | 'TimePicker';

+ 99 - 0
src/components/EasyTable/src/types/pagination.ts

@@ -0,0 +1,99 @@
+import Pagination from 'ant-design-vue/lib/pagination';
+import { VNodeChild } from 'vue';
+
+interface PaginationRenderProps {
+  page: number;
+  type: 'page' | 'prev' | 'next';
+  originalElement: any;
+}
+
+export declare class PaginationConfig extends Pagination {
+  position?: 'top' | 'bottom' | 'both';
+}
+export interface PaginationProps {
+  /**
+   * total number of data items
+   * @default 0
+   * @type number
+   */
+  total?: number;
+
+  /**
+   * default initial page number
+   * @default 1
+   * @type number
+   */
+  defaultCurrent?: number;
+
+  /**
+   * current page number
+   * @type number
+   */
+  current?: number;
+
+  /**
+   * default number of data items per page
+   * @default 10
+   * @type number
+   */
+  defaultPageSize?: number;
+
+  /**
+   * number of data items per page
+   * @type number
+   */
+  pageSize?: number;
+
+  /**
+   * Whether to hide pager on single page
+   * @default false
+   * @type boolean
+   */
+  hideOnSinglePage?: boolean;
+
+  /**
+   * determine whether pageSize can be changed
+   * @default false
+   * @type boolean
+   */
+  showSizeChanger?: boolean;
+
+  /**
+   * specify the sizeChanger options
+   * @default ['10', '20', '30', '40']
+   * @type string[]
+   */
+  pageSizeOptions?: string[];
+
+  /**
+   * determine whether you can jump to pages directly
+   * @default false
+   * @type boolean
+   */
+  showQuickJumper?: boolean | object;
+
+  /**
+   * to display the total number and range
+   * @type Function
+   */
+  showTotal?: (total: number, range: [number, number]) => any;
+
+  /**
+   * specify the size of Pagination, can be set to small
+   * @default ''
+   * @type string
+   */
+  size?: string;
+
+  /**
+   * whether to setting simple mode
+   * @type boolean
+   */
+  simple?: boolean;
+
+  /**
+   * to customize item innerHTML
+   * @type Function
+   */
+  itemRender?: (props: PaginationRenderProps) => VNodeChild | JSX.Element;
+}

+ 441 - 0
src/components/EasyTable/src/types/table.ts

@@ -0,0 +1,441 @@
+import type { VNodeChild } from 'vue';
+import type { PaginationProps } from './pagination';
+import type { FormProps } from '/@/components/Form';
+import type {
+  ColumnProps,
+  TableRowSelection as ITableRowSelection,
+} from 'ant-design-vue/lib/table/interface';
+
+import { ComponentType } from './componentType';
+import { VueNode } from '/@/utils/propTypes';
+import { RoleEnum } from '/@/enums/roleEnum';
+
+export declare type SortOrder = 'ascend' | 'descend';
+
+export interface TableCurrentDataSource<T = Recordable> {
+  currentDataSource: T[];
+}
+
+export interface TableRowSelection<T = any> extends ITableRowSelection {
+  /**
+   * Callback executed when selected rows change
+   * @type Function
+   */
+  onChange?: (selectedRowKeys: string[] | number[], selectedRows: T[]) => any;
+
+  /**
+   * Callback executed when select/deselect one row
+   * @type FunctionT
+   */
+  onSelect?: (record: T, selected: boolean, selectedRows: Object[], nativeEvent: Event) => any;
+
+  /**
+   * Callback executed when select/deselect all rows
+   * @type Function
+   */
+  onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => any;
+
+  /**
+   * Callback executed when row selection is inverted
+   * @type Function
+   */
+  onSelectInvert?: (selectedRows: string[] | number[]) => any;
+}
+
+export interface TableCustomRecord<T> {
+  record?: T;
+  index?: number;
+}
+
+export interface ExpandedRowRenderRecord<T> extends TableCustomRecord<T> {
+  indent?: number;
+  expanded?: boolean;
+}
+export interface ColumnFilterItem {
+  text?: string;
+  value?: string;
+  children?: any;
+}
+
+export interface TableCustomRecord<T = Recordable> {
+  record?: T;
+  index?: number;
+}
+
+export interface SorterResult {
+  column: ColumnProps;
+  order: SortOrder;
+  field: string;
+  columnKey: string;
+}
+
+export interface FetchParams {
+  searchInfo?: Recordable;
+  page?: number;
+  sortInfo?: Recordable;
+  filterInfo?: Recordable;
+}
+
+export interface GetColumnsParams {
+  ignoreIndex?: boolean;
+  ignoreAction?: boolean;
+  sort?: boolean;
+}
+
+export type SizeType = 'default' | 'middle' | 'small' | 'large';
+
+export interface TableActionType {
+  getSelectRows: <T = Recordable>() => T[];
+  clearSelectedRowKeys: () => void;
+  showTableSearch: () => void;
+  getSelectRowKeys: () => string[];
+  deleteSelectRowByKey: (key: string) => void;
+  setPagination: (info: Partial<PaginationProps>) => void;
+  setTableData: <T = Recordable>(values: T[]) => void;
+  updateTableDataRecord: (rowKey: string | number, record: Recordable) => Recordable | void;
+  getColumns: (opt?: GetColumnsParams) => BasicColumn[];
+  setColumns: (columns: BasicColumn[] | string[]) => void;
+  getDataSource: <T = Recordable>() => T[];
+  setLoading: (loading: boolean) => void;
+  setProps: (props: Partial<BasicTableProps>) => void;
+  redoHeight: () => void;
+  setSelectedRowKeys: (rowKeys: string[] | number[]) => void;
+  getPaginationRef: () => PaginationProps | boolean;
+  getSize: () => SizeType;
+  getRowSelection: () => TableRowSelection<Recordable>;
+  getCacheColumns: () => BasicColumn[];
+  emit?: EmitType;
+  updateTableData: (index: number, key: string, value: any) => Recordable;
+  setShowPagination: (show: boolean) => Promise<void>;
+  getShowPagination: () => boolean;
+  setCacheColumnsByField?: (dataIndex: string | undefined, value: BasicColumn) => void;
+}
+
+export interface FetchSetting {
+  // 请求接口当前页数
+  pageField: string;
+  // 每页显示多少条
+  sizeField: string;
+  // 请求结果列表字段  支持 a.b.c
+  listField: string;
+  // 请求结果总数字段  支持 a.b.c
+  totalField: string;
+}
+
+export interface TableSetting {
+  redo?: boolean;
+  size?: boolean;
+  setting?: boolean;
+  fullScreen?: boolean;
+  formSearch?: boolean;
+}
+
+export interface BasicTableProps<T = any> {
+  // 点击行选中
+  clickToRowSelect?: boolean;
+  isTreeTable?: boolean;
+  // 自定义排序方法
+  sortFn?: (sortInfo: SorterResult) => any;
+  // 排序方法
+  filterFn?: (data: Partial<Recordable<string[]>>) => any;
+  // 取消表格的默认padding
+  inset?: boolean;
+  // 显示表格设置
+  showTableSetting?: boolean;
+  tableSetting?: TableSetting;
+  // 斑马纹
+  striped?: boolean;
+  // 是否自动生成key
+  autoCreateKey?: boolean;
+  // 计算合计行的方法
+  summaryFunc?: (...arg: any) => Recordable[];
+  // 自定义合计表格内容
+  summaryData?: Recordable[];
+  // 是否显示合计行
+  showSummary?: boolean;
+  // 是否可拖拽列
+  canColDrag?: boolean;
+  // 接口请求对象
+  api?: (...arg: any) => Promise<any>;
+  // 请求之前处理参数
+  beforeFetch?: Fn;
+  // 自定义处理接口返回参数
+  afterFetch?: Fn;
+  // 查询条件请求之前处理
+  handleSearchInfoFn?: Fn;
+  // 请求接口配置
+  fetchSetting?: FetchSetting;
+  // 立即请求接口
+  immediate?: boolean;
+  // 在开起搜索表单的时候,如果没有数据是否显示表格
+  emptyDataIsShowTable?: boolean;
+  // 额外的请求参数
+  searchInfo?: Recordable;
+  // 使用搜索表单
+  useSearchForm?: boolean;
+  // 表单配置
+  formConfig?: Partial<FormProps>;
+  // 列配置
+  columns: BasicColumn[];
+  // 是否显示序号列
+  showIndexColumn?: boolean;
+  // 序号列配置
+  indexColumnProps?: BasicColumn;
+  actionColumn?: BasicColumn;
+  // 文本超过宽度是否显示。。。
+  ellipsis?: boolean;
+  // 是否可以自适应高度
+  canResize?: boolean;
+  // 自适应高度偏移, 计算结果-偏移量
+  resizeHeightOffset?: number;
+
+  // 在分页改变的时候清空选项
+  clearSelectOnPageChange?: boolean;
+  //
+  rowKey?: string | ((record: Recordable) => string);
+  // 数据
+  dataSource?: Recordable[];
+  // 标题右侧提示
+  titleHelpMessage?: string | string[];
+  // 表格滚动最大高度
+  maxHeight?: number;
+  // 是否显示边框
+  bordered?: boolean;
+  // 分页配置
+  pagination?: PaginationProps | boolean;
+  // loading加载
+  loading?: boolean;
+
+  /**
+   * The column contains children to display
+   * @default 'children'
+   * @type string | string[]
+   */
+  childrenColumnName?: string;
+
+  /**
+   * Override default table elements
+   * @type object
+   */
+  components?: object;
+
+  /**
+   * Expand all rows initially
+   * @default false
+   * @type boolean
+   */
+  defaultExpandAllRows?: boolean;
+
+  /**
+   * Initial expanded row keys
+   * @type string[]
+   */
+  defaultExpandedRowKeys?: string[];
+
+  /**
+   * Current expanded row keys
+   * @type string[]
+   */
+  expandedRowKeys?: string[];
+
+  /**
+   * Expanded container render for each row
+   * @type Function
+   */
+  expandedRowRender?: (record?: ExpandedRowRenderRecord<T>) => VNodeChild | JSX.Element;
+
+  /**
+   * Customize row expand Icon.
+   * @type Function | VNodeChild
+   */
+  expandIcon?: Function | VNodeChild | JSX.Element;
+
+  /**
+   * Whether to expand row by clicking anywhere in the whole row
+   * @default false
+   * @type boolean
+   */
+  expandRowByClick?: boolean;
+
+  /**
+   * The index of `expandIcon` which column will be inserted when `expandIconAsCell` is false. default 0
+   */
+  expandIconColumnIndex?: number;
+
+  /**
+   * Table footer renderer
+   * @type Function | VNodeChild
+   */
+  footer?: Function | VNodeChild | JSX.Element;
+
+  /**
+   * Indent size in pixels of tree data
+   * @default 15
+   * @type number
+   */
+  indentSize?: number;
+
+  /**
+   * i18n text including filter, sort, empty text, etc
+   * @default { filterConfirm: 'Ok', filterReset: 'Reset', emptyText: 'No Data' }
+   * @type object
+   */
+  locale?: object;
+
+  /**
+   * Row's className
+   * @type Function
+   */
+  rowClassName?: (record: TableCustomRecord<T>) => string;
+
+  /**
+   * Row selection config
+   * @type object
+   */
+  rowSelection?: TableRowSelection;
+
+  /**
+   * Set horizontal or vertical scrolling, can also be used to specify the width and height of the scroll area.
+   * It is recommended to set a number for x, if you want to set it to true,
+   * you need to add style .ant-table td { white-space: nowrap; }.
+   * @type object
+   */
+  scroll?: { x?: number | true; y?: number };
+
+  /**
+   * Whether to show table header
+   * @default true
+   * @type boolean
+   */
+  showHeader?: boolean;
+
+  /**
+   * Size of table
+   * @default 'default'
+   * @type string
+   */
+  size?: SizeType;
+
+  /**
+   * Table title renderer
+   * @type Function | ScopedSlot
+   */
+  title?: VNodeChild | JSX.Element | string | ((data: Recordable) => string);
+
+  /**
+   * Set props on per header row
+   * @type Function
+   */
+  customHeaderRow?: (column: ColumnProps, index: number) => object;
+
+  /**
+   * Set props on per row
+   * @type Function
+   */
+  customRow?: (record: T, index: number) => object;
+
+  /**
+   * `table-layout` attribute of table element
+   * `fixed` when header/columns are fixed, or using `column.ellipsis`
+   *
+   * @see https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout
+   * @version 1.5.0
+   */
+  tableLayout?: 'auto' | 'fixed' | string;
+
+  /**
+   * the render container of dropdowns in table
+   * @param triggerNode
+   * @version 1.5.0
+   */
+  getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement;
+
+  /**
+   * Data can be changed again before rendering.
+   * The default configuration of general user empty data.
+   * You can configured globally through [ConfigProvider](https://antdv.com/components/config-provider-cn/)
+   *
+   * @version 1.5.4
+   */
+  transformCellText?: Function;
+
+  /**
+   * Callback executed when pagination, filters or sorter is changed
+   * @param pagination
+   * @param filters
+   * @param sorter
+   * @param currentDataSource
+   */
+  onChange?: (pagination: any, filters: any, sorter: any, extra: any) => void;
+
+  /**
+   * Callback executed when the row expand icon is clicked
+   *
+   * @param expanded
+   * @param record
+   */
+  onExpand?: (expande: boolean, record: T) => void;
+
+  /**
+   * Callback executed when the expanded rows change
+   * @param expandedRows
+   */
+  onExpandedRowsChange?: (expandedRows: string[] | number[]) => void;
+
+  onColumnsChange?: (data: ColumnChangeParam[]) => void;
+}
+
+export type CellFormat =
+  | string
+  | ((text: string, record: Recordable, index: number) => string | number)
+  | Map<string | number, any>;
+
+// @ts-ignore
+export interface BasicColumn extends ColumnProps {
+  children?: BasicColumn[];
+  filters?: {
+    text: string;
+    value: string;
+    children?:
+      | unknown[]
+      | (((props: Record<string, unknown>) => unknown[]) & (() => unknown[]) & (() => unknown[]));
+  }[];
+
+  //
+  flag?: 'INDEX' | 'DEFAULT' | 'CHECKBOX' | 'RADIO' | 'ACTION';
+  customTitle?: VueNode;
+
+  slots?: Recordable;
+
+  // Whether to hide the column by default, it can be displayed in the column configuration
+  defaultHidden?: boolean;
+
+  // Help text for table column header
+  helpMessage?: string | string[];
+
+  format?: CellFormat;
+
+  // Editable
+  edit?: boolean;
+  editRow?: boolean;
+  editable?: boolean;
+  editComponent?: ComponentType;
+  editComponentProps?: Recordable;
+  editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
+  editValueMap?: (value: any) => string;
+  onEditRow?: () => void;
+  // 权限编码控制是否显示
+  auth?: RoleEnum | RoleEnum[] | string | string[];
+  // 业务控制是否显示
+  ifShow?: boolean | ((column: BasicColumn) => boolean);
+}
+
+export type ColumnChangeParam = {
+  dataIndex: string;
+  fixed: boolean | 'left' | 'right' | undefined;
+  visible: boolean;
+};
+
+export interface InnerHandlers {
+  onColumnsChange: (data: ColumnChangeParam[]) => void;
+}

+ 24 - 0
src/components/EasyTable/src/types/tableAction.ts

@@ -0,0 +1,24 @@
+import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
+import { RoleEnum } from '/@/enums/roleEnum';
+export interface ActionItem extends ButtonProps {
+  onClick?: Fn;
+  label: string;
+  color?: 'success' | 'error' | 'warning';
+  icon?: string;
+  popConfirm?: PopConfirm;
+  disabled?: boolean;
+  divider?: boolean;
+  // 权限编码控制是否显示
+  auth?: RoleEnum | RoleEnum[] | string | string[];
+  // 业务控制是否显示
+  ifShow?: boolean | ((action: ActionItem) => boolean);
+}
+
+export interface PopConfirm {
+  title: string;
+  okText?: string;
+  cancelText?: string;
+  confirm: Fn;
+  cancel?: Fn;
+  icon?: string;
+}

+ 4 - 0
src/components/Form/src/componentMap.ts

@@ -24,6 +24,8 @@ import FamilyArrCom from './components/FamilyArrCom.vue';
 import CarArrayCom from './components/CarArrayCom.vue';
 import RangeNumber from './components/RangeNumber.vue';
 import ApiSelect from './components/ApiSelect.vue';
+import UploadFile from './components/upload/UploadFile.vue';
+import UploadImage from './components/upload/UploadImage.vue';
 import { BasicUpload } from '/@/components/Upload';
 import { StrengthMeter } from '/@/components/StrengthMeter';
 import { IconPicker } from '/@/components/Icon';
@@ -42,6 +44,8 @@ componentMap.set('Select', Select);
 componentMap.set('ApiSelect', ApiSelect);
 componentMap.set('FamilyArrCom', FamilyArrCom);
 componentMap.set('CarArrayCom', CarArrayCom);
+componentMap.set('UploadFile', UploadFile);
+componentMap.set('UploadImage', UploadImage);
 // componentMap.set('SelectOptGroup', Select.OptGroup);
 // componentMap.set('SelectOption', Select.Option);
 componentMap.set('TreeSelect', TreeSelect);

+ 93 - 0
src/components/Form/src/components/upload/ChooseModal.vue

@@ -0,0 +1,93 @@
+<template>
+  <BasicModal
+    width="800px"
+    title="选择文件"
+    v-bind="$attrs"
+    @register="register"
+    cancelText="关闭"
+    @ok="handleSumbit"
+  >
+    <EasyTable
+      ref="tableRef"
+      rowKey="id"
+      @register="registerTable"
+      :rowSelection="{ type: type === 'images' || type === 'files' ? 'checkbox' : 'radio' }"
+    >
+      <template #action="{ record, column }">
+        <TableAction :actions="createActions(record, column)" />
+      </template>
+    </EasyTable>
+  </BasicModal>
+</template>
+<script lang="ts">
+  import { defineComponent, onUpdated, ref, toRefs, unref } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { EasyTable, useTable, TableAction, TableActionType } from '/@/components/EasyTable';
+  import { columns } from './data';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { getAttachmentList } from '/@/api/sys/general';
+
+  const props = {
+    value: { type: String, default: '' },
+    type: { type: String, default: '' },
+  };
+  export default defineComponent({
+    components: { BasicModal, EasyTable, TableAction },
+    props,
+    emits: ['register', 'checked'],
+    setup(props, { emit }) {
+      const { createMessage } = useMessage();
+      const { error } = createMessage;
+      const tableRef = ref<Nullable<TableActionType>>(null);
+      const [register, { closeModal }] = useModalInner();
+
+      const [registerTable] = useTable({
+        columns: columns,
+        api: getAttachmentList,
+        showIndexColumn: false,
+        bordered: true,
+        maxHeight: 300,
+      });
+      function getTableAction() {
+        // 获取组件
+        const tableAction = unref(tableRef);
+        if (!tableAction) {
+          throw new Error('tableAction is null');
+        }
+        return tableAction;
+      }
+
+      async function handleSumbit() {
+        const rows = await getTableAction().getSelectRows();
+        if (rows.length) {
+          emit('checked', rows);
+          closeModal();
+          getTableAction().clearSelectedRowKeys();
+        } else {
+          error('请先选择文件!');
+        }
+      }
+      onUpdated(() => {
+        document.onkeydown = function (e) {
+          if (e.key === 'Enter') {
+            handleSumbit();
+          }
+        };
+      });
+
+      return {
+        register,
+        tableRef,
+        registerTable,
+        handleSumbit,
+        getTableAction,
+        ...toRefs(props),
+      };
+    },
+  });
+</script>
+<style lang="less">
+  .check-btn {
+    margin-left: 5px !important;
+  }
+</style>

+ 61 - 0
src/components/Form/src/components/upload/Image.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="wrap">
+    <a-image
+      v-if="isAssetTypeAnImage(src)"
+      class="image"
+      :src="imgUrlPrefix + src"
+      style="width: auto; height: 70px"
+    />
+    <a v-if="!isAssetTypeAnImage(src)" :href="imgUrlPrefix + src"
+      ><i class="fa fa-file file-font" aria-hidden="true"></i
+    ></a>
+  </div>
+</template>
+<script lang="ts">
+  import { Image } from 'ant-design-vue';
+  import { defineComponent, toRefs } from 'vue';
+  import { useGlobSetting } from '/@/hooks/setting';
+
+  const props = {
+    src: { type: String, default: '' },
+  };
+
+  export default defineComponent({
+    components: { aImage: Image },
+    props,
+    setup(props) {
+      const { imgUrlPrefix } = useGlobSetting();
+
+      function isAssetTypeAnImage(filePath) {
+        //获取最后一个.的位置
+        const index = filePath.lastIndexOf('.');
+        //获取后缀
+        const ext = filePath.substr(index + 1);
+        return (
+          ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff'].indexOf(
+            ext.toLowerCase()
+          ) !== -1
+        );
+      }
+      return {
+        isAssetTypeAnImage,
+        imgUrlPrefix,
+        ...toRefs(props),
+      };
+    },
+  });
+</script>
+<style scoped>
+  .wrap {
+    display: flex;
+    justify-content: space-around;
+    width: 100%;
+    height: 80px;
+    align-items: center;
+    border: 1px solid #ddd;
+    border-radius: 2px;
+  }
+  .file-font {
+    font-size: 60px;
+  }
+</style>

+ 207 - 0
src/components/Form/src/components/upload/UploadFile.vue

@@ -0,0 +1,207 @@
+<template>
+  <div>
+    <div style="display: flex">
+      <Input class="upload-input" :value="fileUrls" disabled />
+      <BasicUpload
+        :maxSize="20"
+        :maxNumber="fileNum"
+        @change="handleChange"
+        :api="uploadApi"
+        class="my-3"
+      />
+      <a-button
+        @click="openChooseModal"
+        style="position: relative; top: 12px; margin: 0 5px"
+        type="danger"
+        preIcon="ic:sharp-view-list"
+        :iconSize="16"
+      >
+        选择
+      </a-button>
+      <span class="tip-span" v-if="tipShow">{{ tip }}</span>
+    </div>
+
+    <div class="file-list" v-if="file_list.length">
+      <div v-for="(item, index) in file_list" :key="index">
+        <div class="file-item" v-if="item !== ''">
+          <div class="file-wrap">
+            <a-image v-if="isAssetTypeAnImage(item)" width="70px" :src="imgUrlPrefix + item" />
+            <a
+              v-if="!isAssetTypeAnImage(item)"
+              :href="imgUrlPrefix + item"
+              :download="getFilename(item)"
+              ><i class="fa fa-file file-font" aria-hidden="true"></i
+            ></a>
+          </div>
+          <div class="dele-file" @click="deleteFile(item, index)">
+            <Icon icon="ri:delete-bin-5-fill" />
+          </div>
+        </div>
+      </div>
+    </div>
+    <ChooseModal :type="type" @register="chooseModalRegister" @checked="checked" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, watch, reactive, toRefs, ref } from 'vue';
+  import { BasicUpload } from '/@/components/Upload';
+  // import { useMessage } from '/@/hooks/web/useMessage';
+  import { uploadApi } from '/@/api/sys/upload';
+  import ChooseModal from './ChooseModal.vue';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { useModal } from '/@/components/Modal';
+  import { Image, Input } from 'ant-design-vue';
+  import Icon from '/@/components/Icon';
+
+  const props = {
+    value: { type: String, default: '' },
+    tip: { type: String, default: '' },
+    type: { type: String, default: '' },
+    errMsg: { type: String, default: '' },
+    rules: { type: Array, default: [] },
+  };
+  interface PropsType {
+    value: string;
+    tip: string;
+    type: string;
+    errMsg: string;
+    rules: [];
+  }
+  export default defineComponent({
+    components: { BasicUpload, ChooseModal, aImage: Image, Input, Icon },
+    props,
+    emits: ['change'],
+    setup(props, { emit }) {
+      // const { createMessage } = useMessage();
+      const fileNum = ref(1);
+      if (props.type === 'files') {
+        fileNum.value = 10;
+      }
+      const { imgUrlPrefix } = useGlobSetting();
+      const [chooseModalRegister, { openModal }] = useModal();
+      function openChooseModal() {
+        openModal(true);
+      }
+      const state = reactive({
+        fileUrls: props.value,
+        file_list: [] as string[],
+        tipShow: false,
+      });
+      state.file_list = props.value.split(',');
+      function deleteFile(url, index) {
+        console.log(`url`, url);
+        const arr = state.fileUrls.split(',');
+        arr.splice(index, 1);
+        state.fileUrls = arr.toString();
+        emit('change', state.fileUrls);
+      }
+      function checked(files) {
+        if (props.type === 'image') {
+          state.fileUrls = files[0].url;
+        } else {
+          let urls: string[] = [];
+          files.map((item) => {
+            urls.push(item.url);
+          });
+          state.fileUrls += ',' + urls.toString();
+        }
+        emit('change', state.fileUrls);
+      }
+
+      watch(props, (props: PropsType) => {
+        state.file_list = props.value.split(',');
+      });
+      function getFilename(filePath) {
+        //获取最后一个\的位置
+        const index = filePath.lastIndexOf('\\');
+        //获取后缀
+        const filename = filePath.substr(index + 1);
+        return filename;
+      }
+
+      function isAssetTypeAnImage(filePath) {
+        //获取最后一个.的位置
+        const index = filePath.lastIndexOf('.');
+        //获取后缀
+        const ext = filePath.substr(index + 1);
+        return (
+          ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff'].indexOf(
+            ext.toLowerCase()
+          ) !== -1
+        );
+      }
+      return {
+        handleChange: (list: string[]) => {
+          if (props.type === 'file') {
+            state.fileUrls = list[0];
+          } else {
+            state.fileUrls = list.toString();
+          }
+          emit('change', state.fileUrls);
+        },
+        checked,
+        fileNum,
+        imgUrlPrefix,
+        getFilename,
+        isAssetTypeAnImage,
+        openChooseModal,
+        deleteFile,
+        uploadApi,
+        chooseModalRegister,
+        openModal,
+        ...toRefs(props),
+        ...toRefs(state),
+      };
+    },
+  });
+</script>
+<style scoped>
+  .upload-input {
+    position: relative;
+    top: 11px;
+    width: 45%;
+    height: 34px;
+    margin-right: 5px;
+  }
+
+  .file-list {
+    display: flex;
+    flex-wrap: wrap;
+    width: 70%;
+  }
+
+  .file-item {
+    width: 120px;
+    margin: 0 5px;
+    text-align: center;
+  }
+
+  .file-wrap {
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    width: 100%;
+    height: 80px;
+    border: 1px solid #ddd;
+    border-radius: 2px;
+  }
+  .file-font {
+    font-size: 60px;
+  }
+
+  .dele-file {
+    height: 20px;
+    margin: 8px 0;
+    color: #fff;
+    background: #e74c3c;
+    border-radius: 2px;
+  }
+
+  .tip-span {
+    position: relative;
+    top: 11px;
+    font-size: 13px;
+    line-height: 250%;
+    color: gray;
+  }
+</style>

+ 180 - 0
src/components/Form/src/components/upload/UploadImage.vue

@@ -0,0 +1,180 @@
+<template>
+  <div>
+    <div style="display: flex">
+      <Input class="upload-input" :value="imageUrls" disabled />
+      <BasicUpload
+        :maxSize="20"
+        :maxNumber="imgNum"
+        @change="handleChange"
+        :api="uploadApi"
+        :accept="['jpg', 'jpeg', 'gif', 'png', 'webp']"
+        class="my-3"
+      />
+      <a-button
+        @click="openChooseModal"
+        style="position: relative; top: 12px; margin: 0 5px"
+        type="danger"
+        preIcon="ic:sharp-view-list"
+        :iconSize="16"
+      >
+        选择
+      </a-button>
+      <span class="tip-span" v-if="tipShow">{{ tip }}</span>
+    </div>
+
+    <div class="image-list" v-if="file_list.length">
+      <div v-for="(item, index) in file_list" :key="index">
+        <div class="image-item" v-if="item !== ''">
+          <div class="image-wrap"> <a-image width="70px" :src="imgUrlPrefix + item" /></div>
+          <div class="dele-image" @click="deleteImage(index)">
+            <Icon icon="ri:delete-bin-5-fill" />
+          </div>
+        </div>
+      </div>
+    </div>
+    <ChooseModal :type="type" @register="chooseModalRegister" @checked="checked" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, watch, reactive, toRefs, ref } from 'vue';
+  import { BasicUpload } from '/@/components/Upload';
+  // import { useMessage } from '/@/hooks/web/useMessage';
+  import { uploadApi } from '/@/api/sys/upload';
+  import ChooseModal from './ChooseModal.vue';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { useModal } from '/@/components/Modal';
+  import { Image, Input } from 'ant-design-vue';
+  import Icon from '/@/components/Icon';
+
+  const props = {
+    value: { type: String, default: '' },
+    tip: { type: String, default: '' },
+    type: { type: String, default: '' },
+    errMsg: { type: String, default: '' },
+    rules: { type: Array, default: [] },
+  };
+  interface PropsType {
+    value: string;
+    tip: string;
+    type: string;
+    errMsg: string;
+    rules: [];
+  }
+  export default defineComponent({
+    components: { BasicUpload, ChooseModal, aImage: Image, Input, Icon },
+    props,
+    emits: ['change'],
+    setup(props, { emit }) {
+      const imgNum = ref(1);
+      if (props.type === 'images') {
+        imgNum.value = 10;
+      }
+      // const { createMessage } = useMessage();
+      const { imgUrlPrefix } = useGlobSetting();
+      const [chooseModalRegister, { openModal }] = useModal();
+      function openChooseModal() {
+        openModal(true);
+      }
+      const state = reactive({
+        imageUrls: props.value,
+        file_list: [] as string[],
+        tipShow: false,
+      });
+      state.file_list = props.value.split(',');
+      function deleteImage(index) {
+        const arr = state.imageUrls.split(',');
+        // arr.splice(
+        //   arr.findIndex((item) => item === url),
+        //   1
+        // );
+        arr.splice(index, 1);
+        state.imageUrls = arr.toString();
+        emit('change', state.imageUrls);
+      }
+      function checked(imgs) {
+        if (props.type === 'image') {
+          state.imageUrls = imgs[0].url;
+        } else {
+          let urls: string[] = [];
+          imgs.map((item) => {
+            urls.push(item.url);
+          });
+          state.imageUrls += ',' + urls.toString();
+        }
+        emit('change', state.imageUrls);
+      }
+
+      watch(props, (props: PropsType) => {
+        state.file_list = props.value.split(',');
+      });
+
+      return {
+        handleChange: (list: string[]) => {
+          if (props.type === 'image') {
+            state.imageUrls = list[0];
+          } else {
+            state.imageUrls = list.toString();
+          }
+          emit('change', state.imageUrls);
+        },
+        imgNum,
+        checked,
+        imgUrlPrefix,
+        openChooseModal,
+        deleteImage,
+        uploadApi,
+        chooseModalRegister,
+        openModal,
+        ...toRefs(props),
+        ...toRefs(state),
+      };
+    },
+  });
+</script>
+<style scoped>
+  .upload-input {
+    position: relative;
+    top: 11px;
+    width: 45%;
+    height: 34px;
+    margin-right: 5px;
+  }
+
+  .image-list {
+    display: flex;
+    flex-wrap: wrap;
+    width: 70%;
+  }
+
+  .image-item {
+    width: 120px;
+    margin: 0 5px;
+    text-align: center;
+  }
+
+  .image-wrap {
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+    width: 100%;
+    height: 80px;
+    border: 1px solid #ddd;
+    border-radius: 2px;
+  }
+
+  .dele-image {
+    height: 20px;
+    margin: 8px 0;
+    color: #fff;
+    background: #e74c3c;
+    border-radius: 2px;
+  }
+
+  .tip-span {
+    position: relative;
+    top: 11px;
+    font-size: 13px;
+    line-height: 250%;
+    color: gray;
+  }
+</style>

+ 98 - 0
src/components/Form/src/components/upload/data.ts

@@ -0,0 +1,98 @@
+import { BasicColumn } from '/@/components/Table';
+import Image from './Image.vue';
+import { h } from 'vue';
+import { Tag } from 'ant-design-vue';
+import { formatSize } from '/@/utils/foramtFileSize';
+import moment from 'moment';
+
+export const columns: BasicColumn[] = [
+  {
+    title: 'ID',
+    align: 'center',
+    dataIndex: 'id',
+    width: 100,
+  },
+  {
+    title: '预览',
+    align: 'center',
+    dataIndex: 'preview',
+    width: 100,
+    customRender: ({ record }) => {
+      return h(Image, {
+        src: record.url,
+        height: '80px',
+      });
+    },
+  },
+  {
+    title: '物理路径',
+    align: 'center',
+    dataIndex: 'url',
+    width: 480,
+    customRender: ({ record }) => {
+      return h(
+        Tag,
+        {
+          color: '#18bc9c',
+          style: {
+            fontSize: '12px',
+            fontWeight: 'bold',
+            margin: '0 auto',
+          },
+        },
+        () => record.url
+      );
+    },
+  },
+  {
+    title: '宽度',
+    align: 'center',
+    dataIndex: 'imagewidth',
+    width: 100,
+  },
+  {
+    title: '高度',
+    align: 'center',
+    dataIndex: 'imageheight',
+    width: 100,
+  },
+  {
+    title: '图片类型',
+    align: 'center',
+    dataIndex: 'imagetype',
+    width: 100,
+  },
+  {
+    title: '储存引擎',
+    align: 'center',
+    dataIndex: 'storage',
+    width: 100,
+  },
+  {
+    title: '文件大小',
+    align: 'center',
+    dataIndex: 'filesize',
+    width: 100,
+    customRender: ({ text = 0 }) => {
+      return formatSize(text);
+    },
+  },
+  {
+    title: 'Mime类型',
+    align: 'center',
+    dataIndex: 'mimetype',
+    width: 100,
+  },
+  {
+    title: '创建日期',
+    align: 'center',
+    dataIndex: 'createtime',
+    width: 160,
+    customRender({ record }) {
+      if (!record.createtime) {
+        return null;
+      }
+      return moment(record.createtime * 1000).format('YYYY-MM-DD HH:mm:ss');
+    },
+  },
+];

+ 2 - 0
src/components/Form/src/types/index.ts

@@ -93,6 +93,8 @@ export type ComponentType =
   | 'FamilyArrCom'
   | 'CarArrayCom'
   | 'CustomInput'
+  | 'UploadFile'
+  | 'UploadImage'
   | 'ApiSelect'
   | 'SelectOptGroup'
   | 'TreeSelect'

+ 4 - 10
src/views/activity/activity/data.ts

@@ -4,7 +4,6 @@ import { adapt } from '/@/utils/adapt';
 import moment from 'moment';
 import { Tinymce } from '/@/components/Tinymce/index';
 import { h } from 'vue';
-import UploadFile from '/@/views/general/config/customComponents/UploadFile.vue';
 import CustomInput from './customCom/CustomInput.vue';
 
 const adaptWidth = adapt();
@@ -433,21 +432,16 @@ export const schemas: FormSchema[] = [
   },
   {
     field: 'attachment',
-    component: 'Select',
+    component: 'UploadFile',
     label: '附件',
     labelWidth: adaptWidth.labelWidth,
     colProps: {
       span: adaptWidth.elContainer,
     },
-    render: ({ model, field }) => {
-      return h(UploadFile, {
-        value: model.attachment,
+    componentProps: () => {
+      return {
         type: 'files',
-        style: { width: '100%' },
-        onChange(value) {
-          model[field] = value;
-        },
-      });
+      };
     },
   },
   {

+ 4 - 10
src/views/activity/meeting/data.ts

@@ -4,7 +4,6 @@ import { adapt } from '/@/utils/adapt';
 import moment from 'moment';
 import { Tinymce } from '/@/components/Tinymce/index';
 import { h } from 'vue';
-import UploadFile from '/@/views/general/config/customComponents/UploadFile.vue';
 import CustomInput from './customCom/CustomInput.vue';
 
 const adaptWidth = adapt();
@@ -403,21 +402,16 @@ export const schemas: FormSchema[] = [
   },
   {
     field: 'attachment',
-    component: 'Select',
+    component: 'UploadFile',
     label: '附件',
     labelWidth: adaptWidth.labelWidth,
     colProps: {
       span: adaptWidth.elContainer,
     },
-    render: ({ model, field }) => {
-      return h(UploadFile, {
-        value: model.attachment,
+    componentProps: () => {
+      return {
         type: 'files',
-        style: { width: '100%' },
-        onChange(value) {
-          model[field] = value;
-        },
-      });
+      };
     },
   },
   {

+ 5 - 12
src/views/bill/bill/data.ts

@@ -8,9 +8,7 @@ import CustomApiSelect from './customCom/CustomApiSelect.vue';
 import CustomApiTypeSelect from './customCom/CustomApiTypeSelect.vue';
 import YearPicker from './customCom/YearPicker.vue';
 import moment from 'moment';
-import { getYearFee } from '/@/api/sys/bill';
-import UploadFile from '/@/views/general/config/customComponents/UploadFile.vue';
-import { getAccountList, getTypeList } from '/@/api/sys/bill';
+import { getYearFee, getAccountList, getTypeList } from '/@/api/sys/bill';
 
 const adaptWidth = adapt();
 
@@ -493,21 +491,16 @@ export const schemas: FormSchema[] = [
   },
   {
     field: 'attachment',
-    component: 'Select',
+    component: 'UploadFile',
     label: '附件',
     labelWidth: adaptWidth.labelWidth,
     colProps: {
       span: adaptWidth.elContainer,
     },
-    render: ({ model, field }) => {
-      return h(UploadFile, {
-        value: model.attachment,
+    componentProps: () => {
+      return {
         type: 'files',
-        style: { width: '100%' },
-        onChange(value) {
-          model[field] = value;
-        },
-      });
+      };
     },
   },
   {

+ 4 - 10
src/views/content/content/data.ts

@@ -1,7 +1,6 @@
 import { FormProps, BasicColumn } from '/@/components/Table';
 import { h } from 'vue';
 // import { Tag } from 'ant-design-vue';
-import UploadFile from '/@/views/general/config/customComponents/UploadFile.vue';
 import { formatToDate } from '/@/utils/dateUtil';
 import { FormSchema } from '/@/components/Form/index';
 import { adapt } from '/@/utils/adapt';
@@ -138,21 +137,16 @@ export const schemas: FormSchema[] = [
   },
   {
     field: 'attachment',
-    component: 'Select',
+    component: 'UploadFile',
     label: '附件',
     labelWidth: adaptWidth.labelWidth,
     colProps: {
       span: adaptWidth.elContainer,
     },
-    render: ({ model, field }) => {
-      return h(UploadFile, {
-        value: model.attachment,
+    componentProps: () => {
+      return {
         type: 'files',
-        style: { width: '100%' },
-        onChange(value) {
-          model[field] = value;
-        },
-      });
+      };
     },
   },
 ];