实现antd Table组件 支持高度固定

1,332 阅读3分钟

功能展示

  • 正常情况下 image.png
  • 展开查询条件也能够动态改变高度

WX20240416-104048@2x.png

  • 数据过多或过少的情况下也能够满足自动撑满屏幕 image.png

代码实现

样式

  • 首先要先自定义table样式, 用于覆盖antd 的默认样式
.ant-spin-nested-loading {
  height: 100%;
  .ant-spin-container {
    height: 100%;
    display: flex;
    flex-direction: column;
  }
  .ant-table-container {
    height: 100%;
    display: flex;
    flex-direction: column;
    .ant-table-content {
      height: 100%; // 数据不够也自动撑满高度
    }
    .ant-table-body {
      position: relative;
      flex: 1;
      table {
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
      }
    }
  }
  .ant-table {
    flex: 1;
  }
}

计算高度

  • 通过自定义的hook,计算高度
import debounce from 'lodash/debounce';
import { RefObject, useEffect, useMemo, useRef, useState } from 'react';

// 直接抄airbnb的visx
//https://github.com/airbnb/visx/blob/master/packages/visx-responsive/src/hooks/useParentSize.ts
interface ResizeObserverEntry {
  contentRect: {
    left: number;
    top: number;
    width: number;
    height: number;
  };
}

type ResizeObserverCallback = (
  entries: ResizeObserverEntry[],
  observer: ResizeObserver
) => void;

export declare class ResizeObserver {
  constructor(callback: ResizeObserverCallback);
  observe(target: Element, options?: any): void;
  unobserve(target: Element): void;
  disconnect(): void;
  static toString(): string;
}

export interface ResizeObserverPolyfill {
  new (callback: ResizeObserverCallback): ResizeObserver;
}

export interface PrivateWindow {
  ResizeObserver: ResizeObserverPolyfill;
}

export type Simplify<T> = { [Key in keyof T]: T[Key] } & {};

export interface DebounceSettings {
  /** Child render updates upon resize are delayed until `debounceTime` milliseconds _after_ the last resize event is observed. Defaults to `300`. */
  debounceTime?: number;
  /** Optional flag to toggle leading debounce calls. When set to true this will ensure that the component always renders immediately. Defaults to `true`. */
  enableDebounceLeadingCall?: boolean;
}

export type ParentSizeState = {
  width: number;
  height: number;
  top: number;
  left: number;
};

export type UseParentSizeConfig = {
  /** Initial size before measuring the parent. */
  initialSize?: Partial<ParentSizeState>;
  /** Optionally inject a ResizeObserver polyfill, else this *must* be globally available. */
  resizeObserverPolyfill?: ResizeObserverPolyfill;
  /** Optional dimensions provided won't trigger a state change when changed. */
  ignoreDimensions?: keyof ParentSizeState | (keyof ParentSizeState)[];
} & DebounceSettings;

type UseParentSizeResult<T extends HTMLElement = HTMLDivElement> =
  ParentSizeState & {
    parentRef: RefObject<T>;
    resize: (state: ParentSizeState) => void;
  };

const defaultIgnoreDimensions: UseParentSizeConfig['ignoreDimensions'] = [];
const defaultInitialSize: ParentSizeState = {
  width: 0,
  height: 0,
  top: 0,
  left: 0,
};

export default function useParentSize<T extends HTMLElement = HTMLDivElement>({
  initialSize = defaultInitialSize,
  debounceTime = 300,
  ignoreDimensions = defaultIgnoreDimensions,
  enableDebounceLeadingCall = true,
  resizeObserverPolyfill,
}: UseParentSizeConfig = {}): UseParentSizeResult<T> {
  const parentRef = useRef<T>(null);
  const animationFrameID = useRef(0);

  const [state, setState] = useState<ParentSizeState>({
    ...defaultInitialSize,
    ...initialSize,
  });

  const resize = useMemo(() => {
    const normalized = Array.isArray(ignoreDimensions)
      ? ignoreDimensions
      : [ignoreDimensions];

    return debounce(
      (incoming: ParentSizeState) => {
        setState((existing) => {
          const stateKeys = Object.keys(existing) as (keyof ParentSizeState)[];
          const keysWithChanges = stateKeys.filter(
            (key) => existing[key] !== incoming[key]
          );
          const shouldBail = keysWithChanges.every((key) =>
            normalized.includes(key)
          );

          return shouldBail ? existing : incoming;
        });
      },
      debounceTime,
      { leading: enableDebounceLeadingCall }
    );
  }, [debounceTime, enableDebounceLeadingCall, ignoreDimensions]);

  useEffect(() => {
    const LocalResizeObserver =
      resizeObserverPolyfill ||
      (window as unknown as PrivateWindow).ResizeObserver;

    const observer = new LocalResizeObserver((entries) => {
      entries.forEach((entry) => {
        const { left, top, width, height } = entry?.contentRect ?? {};
        animationFrameID.current = window.requestAnimationFrame(() => {
          resize({ width, height, top, left });
        });
      });
    });
    if (parentRef.current) observer.observe(parentRef.current);

    return () => {
      window.cancelAnimationFrame(animationFrameID.current);
      observer.disconnect();
      resize.cancel();
    };
  }, [resize, resizeObserverPolyfill]);

  return { parentRef, resize, ...state };
}

设置包裹容器ParentSize

import useParentSize, {
  ParentSizeState,
  UseParentSizeConfig,
} from '@/hooks/useParentSize';
import React from 'react';

export type ParentSizeProvidedProps = ParentSizeState & {
  ref: HTMLDivElement | null;
  resize: (state: ParentSizeState) => void;
};

export type ParentSizeProps = {
  /** Optional `className` to add to the parent `div` wrapper used for size measurement. */
  className?: string;
  /**
   * @deprecated - use `style` prop as all other props are passed directly to the parent `div`.
   * @TODO remove in the next major version.
   * Optional `style` object to apply to the parent `div` wrapper used for size measurement.
   * */
  parentSizeStyles?: React.CSSProperties;
  /** Child render function `({ width, height, top, left, ref, resize }) => ReactNode`. */
  children: (args: ParentSizeProvidedProps) => React.ReactNode;
} & UseParentSizeConfig;

const defaultParentSizeStyles = { width: '100%', height: '100%' };

export default function ParentSize({
  className,
  children,
  debounceTime,
  ignoreDimensions,
  initialSize,
  parentSizeStyles = defaultParentSizeStyles,
  enableDebounceLeadingCall = true,
  resizeObserverPolyfill,
  ...restProps
}: ParentSizeProps &
  Omit<React.HTMLAttributes<HTMLDivElement>, keyof ParentSizeProps>) {
  const { parentRef, resize, ...dimensions } = useParentSize({
    initialSize,
    debounceTime,
    ignoreDimensions,
    enableDebounceLeadingCall,
    resizeObserverPolyfill,
  });

  return (
    <div
      style={parentSizeStyles}
      ref={parentRef}
      className={className}
      {...restProps}>
      {children({
        ...dimensions,
        ref: parentRef.current,
        resize,
      })}
    </div>
  );
}

示例代码使用

import { Table, Form, Input } from 'antd';
import './custom-table.less';

const dataSource = Array.from({ length: 52 }).map((_, i) => {
  return {
    key: i,
    name: '胡彦斌' + i,
    age: 3 + i,
    address: '西湖区湖底公园1号' + i,
  };
});

const columns = [
  {
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: '年龄',
    dataIndex: 'age',
    key: 'age',
  },
  {
    title: '住址',
    dataIndex: 'address',
    key: 'address',
  },
];

function App() {
  return (
    // -86px 模拟layout布局的header
    <div style={{ height: 'calc(100vh - 86px)' }}>
      <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
        {/* 模拟查询 */}
        <div className="search">
          <Form name="basic" layout="inline">
            <Form.Item label="Username" name="username">
              <Input />
            </Form.Item>
            <Form.Item label="UserAge" name="userage">
              <Input />
            </Form.Item>
          </Form>
        </div>
        <ParentSize>
        {({ height }) => {
            return (
            <div style={{height: '100%'}}>
              <Table
                size="small"
                rowKey="name"
                bordered
                dataSource={dataSource}
                columns={columns}
                // y 也可以通过dataSource的length来控制是否展示滚动条, 如数据量很小的情况,就无需显示滚动条
                scroll={{ y: height - 32 }}
              />
              </div>
            )
        }}
          </ParentSize>
      </div>
    </div>
  );
}

export default App;