Vue3 Ant design关于Table的封装

486 阅读7分钟

简易表格

子组件(Table组件)

SchemaForm需要传入的属性
  • ref
  • getFormProps
  • table-instance
  • submit事件
table-instance包含的属性
  • setProps
  • reload
  • fetchData
Table需要传入的属性
  • ref
  • tableProps
  • columns
  • data-source
  • change事件
优化项
  • SchemaForm显/隐控制(getProps.search判断)
<template>
  <div>
    <SchemaForm
      v-if="getProps.search"
      ref="queryFormRef"
      v-bind="getFormProps"
      :table-instance="tableAction"
      @submit="handleSubmit"
    >
    </SchemaForm>
    <Table
      ref="tableRef"
      v-bind="tableProps"
      :columns="innerColumns"
      :data-source="tableData"
      @change="handleTableChange"
    >
    </Table>
  </div>
</template>

<script lang="tsx" setup>
  import { useSlots, computed } from 'vue';
  import { Table } from 'ant-design-vue';
  import {
    useTableMethods,
    createTableContext,
    useTableForm,
    useTableState,
    useColumns,
  } from './hooks';
  import { dynamicTableProps, dynamicTableEmits } from './dynamic-table';
  import type { TableActionType } from './types';
  import { SchemaForm } from '@/components/core/schema-form';

  defineOptions({
    name: 'DynamicTable',
    inheritAttrs: false,
  });

  const props = defineProps(dynamicTableProps);
  const emit = defineEmits(dynamicTableEmits);
  const slots = useSlots();

  // 表格内部状态
  const tableState = useTableState({ props, slots });
  const { tableRef, tableData, queryFormRef, getProps, getBindValues } = tableState;
  // 表格内部方法
  const tableMethods = useTableMethods({ state: tableState, props, emit });
  const { setProps, fetchData, handleSubmit, reload, handleTableChange } = tableMethods;

  const tableAction: TableActionType = {
    setProps,
    reload,
    fetchData,
    isEditable: () => false,
  };

  // 表格列的配置描述
  const { innerColumns } = useColumns({
    props,
    slots,
    state: tableState,
    methods: tableMethods,
    tableAction,
  });

  // 搜索表单
  const tableForm = useTableForm({
    tableState,
    tableMethods,
    slots,
  });
  const { getFormProps } = tableForm;

  // 当前组件所有的状态和方法
  const instance = {
    ...props,
    ...tableState,
    ...tableForm,
    ...tableMethods,
    emit,
  };

  createTableContext(instance);

  fetchData();

  defineExpose(instance);

  const tableProps = computed(() => {
    const { getExpandOption } = tableMethods;
    return {
      ...getBindValues.value,
      ...getExpandOption.value,
    };
  });
</script>

父组件(业务组件)

<template>
  <Card title="查询表单基本使用示例">
    <DynamicTable size="small" bordered :data-request="loadData" :columns="columns" row-key="id">
    </DynamicTable>
  </Card>
</template>

<script lang="ts" setup>
  import { Card } from 'ant-design-vue';
  import { columns, tableData } from './columns';
  import { useTable } from '@/components/core/dynamic-table';

  const [DynamicTable] = useTable();

  const loadData = async (params): Promise<API.TableListResult> => {
    return {
      ...params,
      items: tableData,
    };
  };
</script>

增加hooks

  • 路径:@/components/core/dynamic-table/src/hooks/useTable.tsx
  • 存储Table的ref实例
import { nextTick, ref, unref, watch } from 'vue';
import { isEmpty } from 'lodash-es';
import DynamicTable from '../../index';
import type { FunctionalComponent, Ref } from 'vue';
import type { DynamicTableInstance, DynamicTableProps } from '../dynamic-table';

export function useTable(props?: Partial<DynamicTableProps>) {
  const dynamicTableRef = ref<DynamicTableInstance>({} as DynamicTableInstance);

  async function getTableInstance() {
    await nextTick();
    const table = unref(dynamicTableRef);
    if (isEmpty(table)) {
      console.error('未获取表格实例!');
    }
    return table;
  }

  watch(
    () => props,
    async () => {
      if (props) {
        // console.log('table onMounted', { ...props });
        await nextTick();
        const tableInstance = await getTableInstance();
        tableInstance?.setProps?.(props);
      }
    },
    {
      deep: true,
      flush: 'post',
    },
  );

  const methods = new Proxy<Ref<DynamicTableInstance>>(dynamicTableRef, {
    get(target, key) {
      if (Reflect.has(target, key)) {
        return unref(target);
      }
      if (target.value && Reflect.has(target.value, key)) {
        return Reflect.get(target.value, key);
      }
      return async (...rest) => {
        const table = await getTableInstance();
        return table?.[key]?.(...rest);
      };
    },
  });

  const DynamicTableRender: FunctionalComponent<DynamicTableProps> = (
    compProps,
    { attrs, slots },
  ) => {
    return (
      <DynamicTable
        ref={dynamicTableRef}
        {...{ ...attrs, ...props, ...compProps }}
        v-slots={slots}
      ></DynamicTable>
    );
  };

  return [DynamicTableRender, unref(methods)] as const;
}

useTableState

import { computed, reactive, ref, unref, watch } from 'vue';
import { omit } from 'lodash-es';
import tableConfig from '../dynamic-table.config';
import { useScroll } from './useScroll';
import type { Slots } from 'vue';
import type { DynamicTableProps } from '../dynamic-table';
import type { SchemaFormInstance } from '@/components/core/schema-form';
import type { TableProps, Table } from 'ant-design-vue';
import { useI18n } from '@/hooks/useI18n';

export type Pagination = TableProps['pagination'];
export type TableState = ReturnType<typeof useTableState>;

export type UseTableStateParams = {
  props: DynamicTableProps;
  slots: Slots;
};

interface SearchState {
  sortInfo: Recordable;
  filterInfo: Record<string, string[]>;
}

export const useTableState = ({ props, slots }: UseTableStateParams) => {
  const { t } = useI18n();
  const { scroll } = useScroll({ props });
  /** 表格实例 */
  const tableRef = ref<InstanceType<typeof Table>>();
  /** 查询表单实例 */
  const queryFormRef = ref<SchemaFormInstance>();
  /** 编辑表格的表单实例 */
  const editTableFormRef = ref<SchemaFormInstance>();
  /** 表格数据 */
  const tableData = ref<any[]>([]);
  /** 内部属性 */
  const innerPropsRef = ref<Partial<DynamicTableProps>>();
  /** 分页配置参数 */
  const paginationRef = ref<NonNullable<Pagination>>(false);
  /** 表格加载 */
  const loadingRef = ref<boolean>(!!props.loading);
  /** 编辑表单model */
  const editFormModel = ref<Recordable>({});
  /** 所有验证不通过的表单项 */
  const editFormErrorMsgs = ref(new Map());
  /** 当前所有正在被编辑的行key的格式为:`${recordKey}`  */
  const editableRowKeys = ref(new Set<Key>());
  /** 当前所有正在被编辑的单元格key的格式为:`${recordKey}.${dataIndex}`,仅`editableType`为`cell`时有效  */
  const editableCellKeys = ref(new Set<Key>());
  /** 表格排序或过滤时的搜索参数 */
  const searchState = reactive<SearchState>({
    sortInfo: {},
    filterInfo: {},
  });

  if (!Object.is(props.pagination, false)) {
    paginationRef.value = {
      current: 1,
      pageSize: tableConfig.defaultPageSize,
      total: 0,
      pageSizeOptions: [...tableConfig.pageSizeOptions],
      showQuickJumper: true,
      showSizeChanger: true, // 显示可改变每页数量
      showTotal: (total) => t('component.table.total', { total }), // 显示总数
      // onChange: (current, pageSize) => pageOption?.pageChange?.(current, pageSize),
      // onShowSizeChange: (current, pageSize) => pageOption?.pageChange?.(current, pageSize),
      ...props.pagination,
    };
  }

  // 父组件的props
  const getProps = computed(() => {
    return { ...props, ...unref(innerPropsRef) };
  });

  // table的props
  const getBindValues = computed(() => {
    const props = unref(getProps);

    let propsData: Recordable = {
      ...props,
      rowKey: props.rowKey ?? 'id',
      loading: props.loading ?? unref(loadingRef),
      pagination: unref(paginationRef),
      tableLayout: props.tableLayout ?? 'fixed',
      scroll: unref(scroll),
    };
    if (slots.expandedRowRender) {
      propsData = omit(propsData, 'scroll');
    }

    propsData = omit(propsData, ['class', 'onChange', 'columns']);
    return propsData;
  });

  // 如果外界设置了dataSource,那就直接用外界提供的数据
  watch(
    () => props.dataSource,
    (val) => {
      if (val) {
        tableData.value = val;
      }
    },
    {
      immediate: true,
      deep: true,
    },
  );

  // columns变化,更新innerPropsRef
  watch(
    () => props.columns,
    (val) => {
      if (val) {
        innerPropsRef.value = {
          ...innerPropsRef.value,
          columns: val,
        };
      }
    },
    {
      immediate: true,
      deep: true,
    },
  );

  return {
    tableRef,
    editTableFormRef,
    loadingRef,
    tableData,
    queryFormRef,
    innerPropsRef,
    getProps,
    getBindValues,
    paginationRef,
    editFormModel,
    editFormErrorMsgs,
    editableCellKeys,
    editableRowKeys,
    searchState,
  };
};

useTableMethods

import { unref, nextTick, getCurrentInstance, watch } from 'vue';
import { isObject, isFunction, isBoolean, get } from 'lodash-es';
import { useInfiniteScroll } from '@vueuse/core';
import tableConfig from '../dynamic-table.config';
import { useEditable } from './useEditable';
import { useTableExpand } from './useTableExpand';
import type { DynamicTableProps, DynamicTableEmitFn } from '../dynamic-table';
import type { OnChangeCallbackParams, TableColumn } from '../types/';
import type { Pagination, TableState } from './useTableState';
import type { FormProps } from 'ant-design-vue';
import { warn } from '@/utils/log';

export type UseInfiniteScrollParams = Parameters<typeof useInfiniteScroll>;

export type TableMethods = ReturnType<typeof useTableMethods>;

export type UseTableMethodsContext = {
  state: TableState;
  props: DynamicTableProps;
  emit: DynamicTableEmitFn;
};

export const useTableMethods = ({ state, props, emit }: UseTableMethodsContext) => {
  const {
    innerPropsRef,
    tableData,
    loadingRef,
    queryFormRef,
    paginationRef,
    editFormErrorMsgs,
    searchState,
  } = state;
  // 可编辑行
  const editableMethods = useEditable({ state, props });
  const expandMethods = useTableExpand({ state, props, emit });

  // searchParams更新,重新请求表格数据
  watch(
    () => props.searchParams,
    () => {
      fetchData();
    },
  );

  // 合并props
  const setProps = (props: Partial<DynamicTableProps>) => {
    innerPropsRef.value = { ...unref(innerPropsRef), ...props };
  };

  /**
   * @description 表格查询
   */
  const handleSubmit = (params, page = 1) => {
    // 更新分页信息
    updatePagination({
      current: page,
    });
    // 获取表格数据
    fetchData(params);
  };

  /**
   * @param {object} params 表格查询参数
   * @param {boolean} flush 是否将页数重置到第一页
   * @description 获取表格数据
   */
  const fetchData = async (params = {}) => {
    const { dataRequest, dataSource, fetchConfig, searchParams } = props;

    // 异常情况判断
    if (!dataRequest || !isFunction(dataRequest) || Array.isArray(dataSource)) {
      return;
    }
    try {
      let pageParams: Recordable = {};
      const pagination = unref(paginationRef)!;

      // 合并请求配置项(页码,页数,总条数,res中list的字段名)
      const { pageField, sizeField, listField, totalField } = {
        ...tableConfig.fetchConfig,
        ...fetchConfig,
      };

      // 是否启用了分页
      const enablePagination = isObject(pagination);
      if (enablePagination) {
        pageParams = {
          [pageField]: pagination.current,
          [sizeField]: pagination.pageSize,
        };
      }
      const { sortInfo = {}, filterInfo } = searchState;
      // 表格查询参数
      let queryParams: Recordable = {
        ...pageParams,
        ...sortInfo,
        ...filterInfo,
        ...searchParams,
        ...params,
      };
      await nextTick();
      // 先校验Form
      if (queryFormRef.value) {
        const values = await queryFormRef.value.validate();
        queryParams = {
          // 将Form的值转换为接口需要的格式
          ...queryFormRef.value.handleFormValues(values),
          ...queryParams,
        };
      }

      loadingRef.value = true;
      const res = await dataRequest(queryParams);

      const isArrayResult = Array.isArray(res);
      const resultItems: Recordable[] = isArrayResult ? res : get(res, listField);
      const resultTotal: number = isArrayResult ? res.length : Number(get(res, totalField));

      // 异常情况判断
      if (enablePagination && resultTotal) {
        const { current = 1, pageSize = tableConfig.defaultPageSize } = pagination;
        const currentTotalPage = Math.ceil(resultTotal / pageSize);
        // 当前页大于总页数
        if (current > currentTotalPage) {
          updatePagination({
            current: currentTotalPage,
          });
          return await fetchData(params);
        }
      }
      // 更新表格数据
      tableData.value = resultItems;
      // 更新分页信息-总条数
      updatePagination({ total: ~~resultTotal });
      // 更新分页信息-当前页(不分页时不更新,queryParams中不会包含pageField)
      if (queryParams[pageField]) {
        updatePagination({ current: queryParams[pageField] || 1 });
      }
      return tableData;
    } catch (error) {
      warn(`表格查询出错:${error}`);
      emit('fetch-error', error);
      tableData.value = [];
      updatePagination({ total: 0 });
    } finally {
      loadingRef.value = false;
    }
  };

  /**
   * @description 刷新表格
   */
  const reload = (resetPageIndex = false) => {
    const pagination = unref(paginationRef);
    // resetPageIndex为true时,更新分页信息到第1页
    if (Object.is(resetPageIndex, true) && isObject(pagination)) {
      pagination.current = 1;
    }
    // 获取表格数据
    return fetchData();
  };

  /**
   * @description 分页改变
   */
  const handleTableChange = async (...rest: OnChangeCallbackParams) => {
    const [pagination, filters, sorter] = rest;
    const { sortFn, filterFn } = props;

    // 先校验form
    if (queryFormRef.value) {
      await queryFormRef.value.validate();
    }
    // 更新分页信息
    updatePagination(pagination);

    const params: Recordable = {};
    // 排序时,处理sorter数据,赋值
    if (sorter && isFunction(sortFn)) {
      const sortInfo = sortFn(sorter);
      searchState.sortInfo = sortInfo;
      params.sortInfo = sortInfo;
    }

    // 过滤时,处理filters数据,赋值
    if (filters && isFunction(filterFn)) {
      const filterInfo = filterFn(filters);
      searchState.filterInfo = filterInfo;
      params.filterInfo = filterInfo;
    }

    // 获取表格数据(ant的change事件钩子,无需传新的分页信息和form)
    await fetchData({});
    emit('change', ...rest);
  };

  // dataIndex 可以为 a.b.c
  // const getDataIndexVal = (dataIndex, record) => dataIndex.split('.').reduce((pre, curr) => pre[curr], record)

  // 获取表格列key
  const getColumnKey = (column: TableColumn) => {
    return (column?.key || column?.dataIndex) as string;
  };

  /** 编辑表单验证失败回调 */
  const handleEditFormValidate: FormProps['onValidate'] = (name, status, errorMsgs) => {
    // console.log('errorInfo', editFormErrorMsgs);
    const key = Array.isArray(name) ? name.join('.') : name;
    if (status) {
      editFormErrorMsgs.value.delete(key);
    } else {
      editFormErrorMsgs.value.set(key, errorMsgs);
    }
  };

  /** 更新表格分页信息 */
  const updatePagination = (info: Pagination = paginationRef.value) => {
    if (isBoolean(info)) {
      paginationRef.value = info;
    } else if (isObject(paginationRef.value)) {
      paginationRef.value = {
        ...paginationRef.value,
        ...info,
      };
    }
  };
  /** 表格无限滚动 */
  const onInfiniteScroll = (
    callback: UseInfiniteScrollParams[1],
    options?: UseInfiniteScrollParams[2],
  ) => {
    const el = getCurrentInstance()?.proxy?.$el.querySelector('.ant-table-body');
    useInfiniteScroll(el, callback, options);
  };

  /**
   * @description当外部需要动态改变搜索表单的值或选项时,需要调用此方法获取dynamicFormRef实例
   */
  const getQueryFormRef = () => queryFormRef.value;

  return {
    ...editableMethods,
    ...expandMethods,
    setProps,
    handleSubmit,
    handleTableChange,
    getColumnKey,
    fetchData,
    getQueryFormRef,
    reload,
    onInfiniteScroll,
    handleEditFormValidate,
  };
};

useColumns

import { ref, watchEffect, unref, useSlots, h } from 'vue';
import { cloneDeep, isFunction, mergeWith } from 'lodash-es';
import { EditableCell } from '../components';
import { ColumnKeyFlag, type CustomRenderParams } from '../types/column';
import tableConfig from '../dynamic-table.config';
import type { Slots } from 'vue';
import type {
  TableActionType,
  TableColumn,
  TableMethods,
  TableState,
  DynamicTableProps,
} from '@/components/core/dynamic-table';
import type { FormSchema } from '@/components/core/schema-form';
import { isBoolean } from '@/utils/is';
import { TableAction } from '@/components/core/dynamic-table/src/components';

export type UseTableColumnsContext = {
  state: TableState;
  props: DynamicTableProps;
  methods: TableMethods;
  tableAction: TableActionType;
  slots: Slots;
};

export const useColumns = ({ state, methods, props, tableAction }: UseTableColumnsContext) => {
  const slots = useSlots();
  const innerColumns = ref(props.columns);
  const { getColumnKey } = methods;
  const { getProps } = state;
  const { isEditable } = tableAction;

  watchEffect(() => {
    const innerProps = { ...unref(getProps) };
    const ColumnKeyFlags = Object.keys(ColumnKeyFlag);
    const columns = cloneDeep(innerProps!.columns!.filter((n) => !n.hideInTable));

    // 是否添加序号列
    if (innerProps?.showIndex) {
      columns.unshift({
        dataIndex: 'ACTION',
        title: '序号',
        width: 60,
        align: 'center',
        fixed: 'left',
        ...innerProps?.indexColumnProps,
        customRender: ({ index }) => {
          const getPagination = unref(state.paginationRef);
          if (isBoolean(getPagination)) {
            return index + 1;
          }
          const { current = 1, pageSize = 10 } = getPagination!;
          return ((current < 1 ? 1 : current) - 1) * pageSize + index + 1;
        },
      } as TableColumn);
    }

    // @ts-ignore
    innerColumns.value = columns.map((item) => {
      const customRender = item.customRender;

      const rowKey = props.rowKey as string;
      const columnKey = getColumnKey(item) as string;

      item.align ||= tableConfig.defaultAlign;

      item.customRender = (options) => {
        const { record, index, text } = options as CustomRenderParams<Recordable<any>>;
        /** 当前行是否开启了编辑行模式 */
        const isEditableRow = isEditable(record[rowKey]);
        /** 是否开启了单元格编辑模式 */
        const isEditableCell = innerProps.editableType === 'cell';
        /** 当前单元格是否允许被编辑 */
        const isCellEditable = isBoolean(item.editable)
          ? item.editable
          : item.editable?.(options) ?? true;
        /** 是否允许被编辑 */
        const isShowEditable =
          (isEditableRow || isEditableCell) &&
          isCellEditable &&
          !ColumnKeyFlags.includes(columnKey);

        return isShowEditable
          ? h(
              EditableCell,
              {
                schema: getColumnFormSchema(item, record) as any,
                rowKey: record[rowKey] ?? index,
                editableType: innerProps.editableType,
                column: options,
              },
              { default: () => customRender?.(options) ?? text, ...slots },
            )
          : customRender?.(options);
      };

      // 操作列
      if (item.actions && columnKey === ColumnKeyFlag.ACTION) {
        item.customRender = (options) => {
          const { record, index } = options;
          return h(TableAction, {
            actions: item.actions!(options, tableAction),
            rowKey: record[rowKey] ?? index,
            columnParams: options,
          });
        };
      }
      return {
        key: item.key ?? (item.dataIndex as Key),
        dataIndex: item.dataIndex ?? (item.key as Key),
        ...item,
      } as TableColumn;
    });
  });

  function mergeCustomizer(objValue, srcValue, key) {
    /** 这里着重处理 `componentProps` 为函数时的合并处理 */
    if (key === 'componentProps') {
      return (...rest) => {
        return {
          ...(isFunction(objValue) ? objValue(...rest) : objValue),
          ...(isFunction(srcValue) ? srcValue(...rest) : srcValue),
        };
      };
    }
  }

  /** 获取当前行的form schema */
  const getColumnFormSchema = (item: TableColumn, record: Recordable): FormSchema => {
    const key = getColumnKey(item) as string;
    /** 是否继承搜索表单的属性 */
    const isExtendSearchFormProps = !Object.is(
      item.editFormItemProps?.extendSearchFormProps,
      false,
    );

    return {
      field: `${record[props.rowKey as string]}.${item.searchField ?? key}`,
      component: 'Input',
      defaultValue: record[key],
      colProps: {
        span: unref(getProps).editableType === 'cell' ? 20 : 24,
      },
      formItemProps: {
        help: '',
      },
      ...(isExtendSearchFormProps
        ? mergeWith(cloneDeep(item.formItemProps), item.editFormItemProps, mergeCustomizer)
        : item.editFormItemProps),
    };
  };

  return {
    innerColumns,
  };
};

useTableForm

import { unref, computed, watchEffect } from 'vue';
import { ColumnKeyFlag } from '../types/column';
import type { TableMethods } from './useTableMethods';
import type { TableState } from './useTableState';
import type { ComputedRef, Slots } from 'vue';
import type { FormSchema, SchemaFormProps } from '@/components/core/schema-form';

export type TableForm = ReturnType<typeof useTableForm>;

export type UseTableFormContext = {
  tableState: TableState;
  tableMethods: TableMethods;
  slots: Slots;
};

export function useTableForm({ tableState, slots, tableMethods }: UseTableFormContext) {
  const { getProps, loadingRef } = tableState;
  const { getColumnKey, getQueryFormRef } = tableMethods;

  const getFormProps = computed((): SchemaFormProps => {
    const { formProps } = unref(getProps);
    const { submitButtonOptions } = formProps || {};
    return {
      showAdvancedButton: true,
      layout: 'horizontal',
      labelWidth: 100,
      ...formProps,
      schemas: formProps?.schemas ?? unref(formSchemas),
      submitButtonOptions: { loading: unref(loadingRef), ...submitButtonOptions },
      compact: true,
    };
  });

  const formSchemas = computed<FormSchema[]>(() => {
    const columnKeyFlags = Object.keys(ColumnKeyFlag);
    return unref(getProps)
      .columns.filter((n) => {
        const field = getColumnKey(n);
        return !n.hideInSearch && !!field && !columnKeyFlags.includes(field as string);
      })
      .map((n) => {
        return {
          field: n.searchField ?? (getColumnKey(n) as string),
          component: 'Input',
          label: n.title as string,
          colProps: {
            span: 8,
          },
          ...n.formItemProps,
        };
      })
      .sort((a, b) => Number(a?.order) - Number(b?.order)) as FormSchema[];
  });

  // 同步外部对props的修改
  watchEffect(() => getQueryFormRef()?.setSchemaFormProps(unref(getFormProps)), {
    flush: 'post',
  });

  const getFormSlotKeys: ComputedRef<string[]> = computed(() => {
    const keys = Object.keys(slots);
    return keys
      .map((item) => (item.startsWith('form-') ? item : null))
      .filter((item): item is string => !!item);
  });

  function replaceFormSlotKey(key: string) {
    if (!key) return '';
    return key?.replace?.(/form-/, '') ?? '';
  }

  return {
    getFormProps,
    replaceFormSlotKey,
    getFormSlotKeys,
  };
}

优化项

表格内容自定义

父组件(业务组件)
<template>
  <DynamicTable>
    <template #bodyCell="{ column, record }">
      <template v-if="column.dataIndex === 'name'">
        {{ record.name }} <a class="text-red-500">[测试bodyCell]</a>
      </template>
    </template>
  </DynamicTable>
</template>
子组件(Table组件)
<template>
  <Table>
    <template
      v-for="(_, slotName) of $slots"
      #[slotName]="slotData"
      :key="slotName"
    >
      <slot :name="slotName" v-bind="slotData"></slot>
    </template>
  </Table>
</template>

单行编辑/多行编辑/可编辑单元格

父组件(业务组件)
<template>
  <div>
    <Alert message="可编辑行表格" type="info" show-icon>
      <template #description> 可编辑行表格-可编辑行表格使用示例 </template>
    </Alert>
    <Card title="可编辑行表格基本使用示例" style="margin-top: 20px">
      <DynamicTable
        size="small"
        bordered
        :data-request="loadData"
        :columns="tableColumns"
        :editable-type="editableType"
        :on-save="handleSave"
        :on-cancel="handleCancelSave"
        row-key="id"
      >
        <template #toolbar>
          <Select ref="select" v-model:value="editableType">
            <Select.Option value="single">单行编辑</Select.Option>
            <Select.Option value="multiple">多行编辑</Select.Option>
            <Select.Option value="cell">可编辑单元格</Select.Option>
          </Select>
        </template>
      </DynamicTable>
    </Card>
  </div>
</template>

<script lang="ts" setup>
  import { ref, computed } from 'vue';
  import { Alert, Card, Select, message } from 'ant-design-vue';
  import { columns, tableData } from './columns';
  import type { EditableType, OnSave, OnCancel } from '@/components/core/dynamic-table';
  import { useTable } from '@/components/core/dynamic-table';
  import { waitTime } from '@/utils/common';

  defineOptions({
    name: 'EditRowTable',
  });

  const [DynamicTable] = useTable();

  const editableType = ref<EditableType>('single');

  const loadData = async (params): Promise<API.TableListResult> => {
    console.log('params', params);
    await waitTime(500);

    return {
      ...params,
      items: tableData,
    };
  };

  const tableColumns = computed<typeof columns>(() => [
    ...columns,
    {
      title: '操作',

      hideInTable: editableType.value === 'cell',
      width: 200,
      dataIndex: 'ACTION',
      actions: ({ record }, action) => {
        const { startEditable, cancelEditable, isEditable, validateRow, reload } = action;
        return isEditable(record.id)
          ? [
              {
                label: '保存',
                onClick: async () => {
                  // 获取编辑后的值
                  const result = await validateRow(record.id);
                  message.loading({ content: '保存中...', key: record.id });
                  console.log('保存', result);
                  // TODO 请求接口更新数据
                  await waitTime(2000);
                  // 取消编辑行
                  cancelEditable(record.id);
                  // 刷新表格
                  reload();
                  message.success({ content: '保存成功!', key: record.id, duration: 2 });
                },
              },
              {
                label: '取消',
                onClick: () => {
                  cancelEditable(record.id);
                },
              },
            ]
          : [
              {
                label: '编辑',
                onClick: () => {
                  startEditable(record.id, record);
                },
              },
            ];
      },
    },
  ]);

  const handleCancelSave: OnCancel = (rowKey, record, originRow) => {
    console.log('handleCancelSave', rowKey, record, originRow);
  };

  const handleSave: OnSave = async (rowKey, record, originRow) => {
    console.log('handleSave', rowKey, record, originRow);
    await waitTime(2000);
  };
</script>

<style lang="less" scoped></style>

子组件(Table组件)
<template>
  <div>
    <SchemaForm
      :table-instance="tableAction"
    >
      <!-- ...... -->
    </SchemaForm>
</template>
<script lang="tsx" setup>
  import {
    useTableState,
    useEditable,
  } from './hooks';
  import { dynamicTableProps } from './dynamic-table';
  import type { TableActionType } from './types';

  // ......
  
  const props = defineProps(dynamicTableProps);
  const slots = useSlots();
  
  // 表格内部状态
  const tableState = useTableState({ props, slots });
  // 控制编辑行
  const editableHooks = useEditable({ props, state: tableState });

  const tableAction: TableActionType = {
    // ......
    ...editableHooks,
  };

  // 表格列的配置描述
  const { innerColumns } = useColumns({
    // ......
    tableAction,
  });
  
  // 当前组件所有的状态和方法
  const instance = {
    ...props,
    ...tableState,
    ...tableForm,
    ...tableMethods,
    ...editableHooks,
    ...exportData2ExcelHooks,
    emit,
  };
</script>
useEditable
import { nextTick, watch } from 'vue';
import { cloneDeep } from 'lodash-es';
import { message } from 'ant-design-vue';
import type { DynamicTableProps } from '../dynamic-table';
import type { TableState } from './useTableState';
import type { TableColumn } from '@/components/core/dynamic-table/src/types/column';

type UseTableMethodsContext = {
  state: TableState;
  props: DynamicTableProps;
};

export type UseEditableType = ReturnType<typeof useEditable>;

export const useEditable = ({ state, props }: UseTableMethodsContext) => {
  const {
    tableData,
    editFormModel,
    editTableFormRef,
    editFormErrorMsgs,
    editableCellKeys,
    editableRowKeys,
  } = state;

  watch(
    () => props.editableType,
    (type) => {
      // 清空编辑状态
      if (type === 'cell') {
        editableRowKeys.value.clear();
      } else {
        editableCellKeys.value.clear();
      }
    },
  );

  /** 设置表单值 */
  const setEditFormModel = (recordKey: Key, editValue: Recordable) => {
    Reflect.set(editFormModel.value, recordKey, editValue);
    nextTick(() => {
      editTableFormRef.value?.setFormModel(recordKey, editValue);
    });
  };

  /** 获取要编辑的值 */
  const getEditValue = (
    recordKey: Key,
    currentRow?: Recordable,
    columns?: TableColumn<Recordable<any>>[],
  ) => {
    // 克隆当前行数据作为临时编辑的表单数据,避免直接修改原数据
    const editValue = cloneDeep(
      currentRow ?? tableData.value.find((n) => n[String(props.rowKey)] === recordKey),
    );
    // 用户设置的默认值优先
    columns?.forEach((item) => {
      const { formItemProps, editFormItemProps } = item;
      const field = (item.dataIndex || item.key) as string;
      if (
        !Object.is(editFormItemProps?.extendSearchFormProps, false) &&
        formItemProps &&
        Reflect.has(formItemProps, 'defaultValue')
      ) {
        editValue[field] = formItemProps.defaultValue;
      }
      if (editFormItemProps && Reflect.has(editFormItemProps, 'defaultValue')) {
        editValue[field] = editFormItemProps.defaultValue;
      }
    });
    return editValue;
  };

  /**
   * @description 进入编辑行状态
   *
   * @param recordKey 当前行id,即table的rowKey
   * @param currentRow 当前行数据
   */
  const startEditable = (recordKey: Key, currentRow?: Recordable) => {
    // 先清空可编辑单元格的编辑状态
    editableCellKeys.value.clear();
    // 异常情况判断 - 如果是单行的话,不允许多行编辑
    if (editableRowKeys.value.size > 0 && props.editableType === 'single') {
      message.warn(props.onlyOneLineEditorAlertMessage || '只能同时编辑一行');
      return false;
    }
    // 获取编辑行的默认值
    const editValue = getEditValue(recordKey, currentRow, props.columns);
    // 更新编辑行的默认数据
    setEditFormModel(recordKey, editValue);
    // 开启编辑行状态
    editableRowKeys.value.add(recordKey);
    return true;
  };

  /** 进入编辑单元格状态 */
  const startCellEditable = (recordKey: Key, dataIndex: Key, currentRow?: Recordable) => {
    // 先清空编辑行的编辑状态
    editableRowKeys.value.clear();
    // 获取当前编辑的单元格的配置项
    const targetColumn = props.columns.filter((n) => n.dataIndex === dataIndex);
    // 获取编辑单元格的默认值
    const editValue = getEditValue(recordKey, currentRow, targetColumn);
    // 更新编辑单元格的默认数据
    setEditFormModel(recordKey, {
      // 获取表单编辑前的值
      ...(getEditFormModel(recordKey) || editValue),
      [dataIndex]: editValue[dataIndex],
    });
    // 开启编辑单元格状态
    editableCellKeys.value.add(`${recordKey}.${dataIndex}`);
  };

  /** 取消编辑单元格 */
  const cancelCellEditable = (recordKey: Key, dataIndex: Key) => {
    // 取消当前编辑单元格的编辑状态
    editableCellKeys.value.delete(`${recordKey}.${dataIndex}`);
    // 获取表单编辑后的值
    const formModel = getEditFormModel(recordKey);
    // 获取表单编辑前的值
    const record = tableData.value.find((n) => n[String(props.rowKey)] === recordKey);
    // 取消编辑,还原默认值
    if (record) {
      Reflect.set(formModel, dataIndex, record[dataIndex]);
    }
    // 清掉表单项的校验错误信息
    editFormErrorMsgs.value.delete(`${recordKey}.${dataIndex}`);
  };

  /**
   * 取消编辑行
   *
   * @param recordKey
   */
  const cancelEditable = (recordKey: Key) => {
    // 取消当前编辑行的编辑状态
    editableRowKeys.value.delete(recordKey);
    // 获取表单编辑后的值
    const formModel = getEditFormModel(recordKey);
    // 清掉表单项的校验错误信息
    Object.keys(formModel).forEach((field) =>
      editFormErrorMsgs.value.delete(`${recordKey}.${field}`),
    );

    // 删除开始编辑时插入formModel的行数据
    nextTick(() => {
      editTableFormRef.value?.delFormModel?.(recordKey);
    });

    // 删除开始编辑时插入formModel的行数据
    return Reflect.deleteProperty(editFormModel.value, recordKey);
  };

  /** 这行是不是编辑状态 */
  const isEditable = (recordKey: Key) => editableRowKeys.value.has(recordKey);

  /** 获取表单编辑后的值 */
  const getEditFormModel = (recordKey: Key) => Reflect.get(editFormModel.value, recordKey);

  /** 行编辑表单是否校验通过 */
  const validateRow = async (recordKey: Key) => {
    // 行编辑数据的所有字段名
    const nameList = Object.keys(getEditFormModel(recordKey)).map((n) => [String(recordKey), n]);
    const result = await editTableFormRef.value?.validateFields(nameList);
    return result?.[recordKey] ?? result;
  };

  /**
   * 单元格表单是否校验通过
   * @param recordKey 当前行ID
   * @param dataIndex 当前单元格字段名, eg: `column.dataIndex`
   *  */
  const validateCell = async (recordKey: Key, dataIndex: Key) => {
    const result = await editTableFormRef.value?.validateFields([[String(recordKey), dataIndex]]);
    return result?.[recordKey] ?? result;
  };

  return {
    setEditFormModel,
    startEditable,
    startCellEditable,
    cancelCellEditable,
    cancelEditable,
    isEditable,
    validateRow,
    validateCell,
    getEditFormModel,
  };
};