Vue3 el-table 使用useFixed 完成表格表头固定功能

841 阅读1分钟

我们一个常见的效果是表头固定,随着滚动条在顶部固定,而自带的fixed只是给table设置一个高度,一点也不是我们想要的效果

查看效果

2023-07-24 07.58.25.gif

准备

增加定义

export interface Fixed {
  header?: boolean; // 头部固定(默认表头)
  onHeaderFixed?: () => void; // 头部固定触发事件
  onHeaderUnfixed?: () => void; // 头部不固定触发事件
  footer?: boolean; // 底部固定(默认取分页)
  onFooterFixed?: () => void; // 底部固定触发事件
  onFooterUnfixed?: () => void; // 底部不固定触发事件
}

props

fixed: {
  type: Object as PropType<Fixed>,
  default: () => ({}),
},

useFixed实现

function useFixed(fixed: Fixed = {}, elProTable: Ref) {
  const getScrollElement = (el: any, root = window) => {
    let node = el;
    const overflowScrollReg = /scroll|auto/i;
    const rtv = [];

    while (
      node &&
      node.tagName !== "HTML" &&
      node.nodeType === 1 &&
      node !== root
    ) {
      const { overflowY } = window.getComputedStyle(node);

      if (overflowScrollReg.test(overflowY)) {
        if (node.tagName !== "BODY") {
          rtv.push(node);
        }

        // see: https://github.com/youzan/vant/issues/3823
        const { overflowY: htmlOverflowY } = window.getComputedStyle(
          node.parentNode
        );

        if (overflowScrollReg.test(htmlOverflowY)) {
          rtv.push(node);
        }
      }
      node = node.parentNode;
    }

    // 需要返回最近的一个,el-main也是overflow:auto 但是没有滚动条,只有app-main才有滚动条
    return rtv.length ? rtv[rtv.length - 1] : root;
  };
  const getElementStyle = (
    element: HTMLElement,
    styleKey: keyof CSSStyleDeclaration
  ) => window.getComputedStyle(element)[styleKey];

  (fixed.header || fixed.footer) &&
    onMounted(() => {
      const el = elProTable.value;
      const tableElement = el.querySelector(".el-table");
      const paginationElement = el.querySelector(".el-pagination");
      const tableHeaderElement = el.querySelector(".el-table__header-wrapper");
      const tableBodyElement = el.querySelector(".el-table__body-wrapper");
      const tableInnerWrapperElement = el.querySelector(
        ".el-table__inner-wrapper"
      );
      const scrollElement = getScrollElement(el);
      let ticking = false;
      const onScroll = () => {
        if (!ticking) {
          requestAnimationFrame(() => {
            const headerHeight = tableHeaderElement.clientHeight;
            const rect = el.querySelector(".el-table").getBoundingClientRect();
            const maxZIndex = Array.from(
              tableElement.querySelectorAll("*")
            ).reduce((maxZIndex, element) => {
              const zIndex = getElementStyle(element as HTMLElement, "zIndex");
              return Math.max(
                maxZIndex as number,
                parseInt(zIndex as string) || 0
              );
            }, 0);
            const bodyWidth = getElementStyle(tableBodyElement, "width");

            if (fixed.header && tableHeaderElement) {
              if (rect.top <= 0 && rect.bottom >= headerHeight) {
                fixed.onHeaderFixed && fixed.onHeaderFixed();
                // console.log("fixed-header", bodyWidth);
                tableHeaderElement.style.position = "fixed";
                tableHeaderElement.style.zIndex = `${maxZIndex}`;
                tableHeaderElement.style.top = 0 + "px";
                tableHeaderElement.style.transition = "top .3s";
                tableHeaderElement.style.width = bodyWidth;
                tableInnerWrapperElement.style.marginTop =
                  tableHeaderElement.offsetHeight + "px";
              } else {
                fixed.onHeaderUnfixed && fixed.onHeaderUnfixed();
                tableHeaderElement.style.width = "auto";
                tableInnerWrapperElement.style.marginTop = 0;
                tableHeaderElement.style.position = "static";
              }
            }

            // console.log("rect.top", rect.top, rect.bottom);

            if (fixed.footer && paginationElement) {
              if (
                rect.top <=
                  scrollElement.innerHeight -
                    tableHeaderElement.offsetHeight -
                    paginationElement.offsetHeight &&
                rect.bottom >= scrollElement.innerHeight + 12
              ) {
                fixed.onFooterFixed && fixed.onFooterFixed();
                paginationElement.style.position = "fixed";
                paginationElement.style.padding = "12px 0";
                paginationElement.style.background = "#fff";
                paginationElement.style.zIndex = `${maxZIndex}`;
                paginationElement.style.bottom = 0;
                paginationElement.style.transition = "bottom .3s";
                paginationElement.style.width = bodyWidth;
                tableInnerWrapperElement.style.marginBottom =
                  paginationElement.offsetHeight + "px";
              } else {
                fixed.onFooterUnfixed && fixed.onFooterUnfixed();
                paginationElement.style.width = "auto";
                tableInnerWrapperElement.style.marginBottom = 0;
                paginationElement.style.position = "static";
              }
            }

            ticking = false;
          });
        }
        ticking = true;
      };
      // scrollElementOnScroll
      scrollElement.addEventListener("scroll", onScroll, false);
      onUnmounted(() => {
        scrollElement.removeEventListener("scroll", onScroll, false);
      });

      // tableElementResizeObserver
      const observer = new ResizeObserver(() => {
        // console.log("resize");
        onScroll();
      });
      observer.observe(tableElement);
      onUnmounted(() => {
        observer.disconnect();
      });
    });
}

调用

<h2>支持表头固定、尾固定</h2>
  <el-pro-table
    :data="tableData2"
    :fixed="{ header: true, footer: true }"
    :pagination="{ total: 100 }"
  >
    <el-table-column prop="date" label="Date" />
    <el-table-column prop="name" label="Name" />
    <el-table-column prop="name" label="Name" />
    <el-table-column prop="state" label="State" />
    <el-table-column prop="city" label="City" />
    <el-table-column prop="address" label="Address" width="600" />
    <el-table-column prop="zip" label="Zip" />
    <el-table-column label="Operations">
      <template #default="{ row }">
        <el-button link type="primary" size="small" @click="handleClick">
          编辑
        </el-button>
        <el-button v-if="row.status === 2" link type="primary" size="small">
          删除
        </el-button>
      </template>
    </el-table-column>
  </el-pro-table>