【实战】基于 vue3-easy-data-table 的移动端表格组件封装

1,881 阅读5分钟

前言

表格是个很常见的东西,在PC端,表格组件的很强大,各个pc端 ui库均有表格组件。

在移动端,这方面的组件比较匮乏,对于组件的想法:轻量化、可定制、有筛选、排序、浮动、分页器等功能;

最终选择 vue3-easy-data-table作为组件封装的核心插件。

组件实现的功能:

  1. 表头插槽、单元格插槽
  2. 分页器自定义开启、自定义
  3. 下拉加载更多
  4. 字段排序
  5. 数据筛选
  6. 列浮动

开发

下拉加载

对于下拉加载的功能,组件本身没有提供对应的api或者本身实现,所以需要自行开发下拉加载功能

useScrollNearBottom.js

useScrollNearBottom 底层是侦听 dom的滚动距离是否小于定义的触底距离,若小于触底距离,则执行回调。

/**
 * @description: 元素滚动距离侦听
 * @param {Element} element
 * @param {Function} callback 触底后的回调
 * @param {Number} threshold 触底距离最小值
 */
function listenForScrollNearBottom(element, callback, threshold = 50) {
  // 确保元素是有效的
  if (!element) {
    console.error("Invalid element provided");
    return;
  }
  const targetForListener = element === window ? window : element;
  // 滚动事件处理函数
  function handleScroll() {
    // 获取元素的滚动高度和总高度
    const scrollTop = element.scrollTop || document.documentElement.scrollTop;
    const scrollHeight =
      element.scrollHeight || document.documentElement.scrollHeight;
    const clientHeight =
      element.clientHeight || document.documentElement.clientHeight;

    // 计算距离底部的距离
    const distanceToBottom = scrollHeight - scrollTop - clientHeight;

    // 检查是否接近底部
    if (distanceToBottom <= threshold) {
      // 在这里可以触发加载更多内容或其他操作
      callback && callback();
    }
  }

  // 添加滚动事件监听器到元素或全局对象(如window)
  function listener() {
    targetForListener.addEventListener("scroll", handleScroll);
  }
  // 卸载侦听
  function removeListener() {
    targetForListener.removeEventListener("scroll", handleScroll);
  }

  return {
    listener,
    removeListener
  };
}

export default listenForScrollNearBottom;

ListTable.vue

开发构思

vue3-easy-data-table拥有两种模式:client-sideserver-side模式,client-side模式适用于所有的数据已经加载完成的情况,换句话说,你的初始请求已经从服务端取到了所有的数据。server-side模式则是指每次跳转到新页面时都需要向服务器请求新的一页的数据。client-side是默认模式,你需要同时使用server-options and server-items-length属性来切换到server-side模式。

这次的封装,主要基于 server-side 模式,即动态数据加载,prop参数的作用:

  • data 视图数据
  • pagesize 通过动态的 prop.pagesize来动态加载渲染的视图数据;
  • offset 滚动触底距离
  • maxLimit 判断数据的 最大加载条数来触发 load加载事件
  • pageNumber 当前显示的页数
  • pageSize 表格数据加载显示的数据条数,vue3-easy-data-table视图显示的数据条数由该参数决定
  • rowsItems 分页器条数选项配置
  • height 表格容器高度,vue3-easy-data-table默认的高度是可视区的高度,即铺满整屏
  • showPage 分页器开关
  • column 表格列配置 { value:'绑定的key', text: '显示的列表头文字'}
  • sortBy 当表格设置排序时,默认显示排序字段
  • sortType 当表格设置排序时,默认的排序规则

数据动态加载,分为两种场景:

  1. 不开启分页,下拉加载

data的数据必须是完整的显示数据,并且pagesize的值应该是data的长度vue3-easy-data-table底层是根据serverOptions.rowsPerPage的值来决定显示的视图数据数量,组件通过侦听props.pagesize来动态更新serverOptions.rowsPerPage,从而实现视图加载

  1. 开启分页

这个场景就很常规了,一般就是切分页或者切条数,重新调用接口数据。切换分页和显示条数时组件会分发pagination事件,或者通过v-model:pageNumberv-model:pageSize,通过侦听分页操作,来调用接口

getViewPortHeight.js

// 获取浏览器窗口的可视区域的高度
export function getViewPortHeight() {
  return document.documentElement.clientHeight || document.body.clientHeight;
}

表格组件封装

<template>
  <div ref="tableRef" class="easy-table-container">
    <Vue3EasyDataTable
      ref="easyTableRef"
      v-model:server-options="serverOptions"
      :server-items-length="maxLimit"
      theme-color="var(--van-primary-color)"
      buttons-pagination
      rows-per-page-message=""
      rows-of-page-separator-message="共"
      :loading="loading"
      :headers="column"
      :items="data"
      :table-height="height"
      :rows-items="rowsItems"
      :hide-footer="!showPage"
      v-bind="$attrs"
      v-on="$attrs"
    >
      <template #empty-message> 暂无数据 </template>
      // 重写分页器
      <template #pagination>
        <van-pagination
          v-model="serverOptions.page"
          :total-items="maxLimit"
          :items-per-page="serverOptions.rowsPerPage"
          force-ellipses
        >
          <template #prev-text>
            <van-icon name="arrow-left" />
          </template>
          <template #next-text>
            <van-icon name="arrow" />
          </template>
        </van-pagination>
      </template>
      <!-- 动态插槽 -->
      <template v-for="(slot, name) in $slots" #[name]="scope" :key="name">
        <slot :name="name" v-bind="scope || {}"></slot>
      </template>
    </Vue3EasyDataTable>
  </div>
</template>
<script setup>
import Vue3EasyDataTable from "vue3-easy-data-table";
import "vue3-easy-data-table/dist/style.css";
import useScrollNearBottom from "@/hooks/useScrollNearBottom.js";
import { getViewPortHeight } from "@/utils/dom";

defineOptions({
  inheritAttrs: false
});

const props = defineProps({
  // 加载状态
  loading: {
    type: Boolean,
    default: false
  },
  // 触底距离
  offset: {
    type: Number,
    default: 50
  },
  // 加载的最大条数
  maxLimit: {
    type: Number,
    default: 50
  },
  // 当前页
  pageNumber: {
    type: Number,
    default: 1
  },
  // 加载条数
  pageSize: {
    type: Number,
    default: 10
  },
  // 分页器配置
  rowsItems: {
    type: Array,
    default: () => [10, 20, 50]
  },
  // 显示分页器
  showPage: {
    type: Boolean,
    default: false
  },
  // 容器高度
  height: {
    type: Number,
    default: 0
  },
  // 渲染数据
  data: {
    type: Array,
    default: () => []
  },
  // 表格字段配置
  column: {
    type: Array,
    default: () => []
  },
  // 默认显示排序字段
  sortBy: {
    type: [Array, String],
    default: () => []
  },
  // 排序规则
  sortType: {
    type: [Array, String],
    default: () => []
  }
});

const emit = defineEmits([
  "load",
  "update:sortType",
  "update:sortBy",
  "update:pageNumber",
  "update:pageSize",
  "pagination"
]);

/** dom ref */
const tableRef = ref();
const easyTableRef = ref();

// 组件滚动侦听
const listener = ref();
const loading = computed(() => props.loading); // 加载
const data = computed(() => props.data);
const height = computed(() => props.height || getViewPortHeight() - 58);
const pageSize = computed(() => props.pageSize);
const sortBy = computed(() => props.sortBy);
const sortType = computed(() => props.sortType);
const serverOptions = ref({
  page: props.pageNumber,
  rowsPerPage: props.pageSize,
  sortBy: sortBy,
  sortType: sortType
}); // 分页、排序配置;

// 排序规则变动
watch(
  () => serverOptions.value.sortType,
  val => {
    emit("update:sortType", val);
  }
);
watch(
  () => serverOptions.value.sortBy,
  val => {
    emit("update:sortBy", val);
  }
);

// 分页条数变动
watch(pageSize, val => {
  serverOptions.value.rowsPerPage = val;
});
// 侦听分页操作
watch(
  [() => serverOptions.value.rowsPerPage, () => serverOptions.value.page],
  () => {
    updatePagination();
  }
);

function updatePagination() {
  const pages = {
    pageNumber: serverOptions.value.page,
    pageSize: serverOptions.value.rowsPerPage
  };
  emit("update:pageNumber", pages.pageNumber);
  emit("update:pageSize", pages.pageSize);
  emit("pagination", pages);
}

// 触底请求加载
function onLoad() {
  // 加载中或超过最大条数,不加载
  if (loading.value || props.maxLimit <= data.value.length) return;
  emit("load");
}


// 初始化滚动侦听
function initScroll() {
  nextTick(() => {
    const dom = tableRef.value.querySelector(".vue3-easy-data-table__main");
    listener.value = useScrollNearBottom(
      dom,
      () => {
        onLoad();
      },
      props.offset
    );
    listener.value.listener();
  });
}

onMounted(() => {
  initScroll();
});

onUnmounted(() => {
  listener.value.removeListener();
});

defineExpose({});
</script>
<style lang="less" scoped>
.easy-table-container {
  overflow-x: hidden;
  max-width: 100vw;

  --van-pagination-font-size: 12px;
  --van-pagination-height: 20px;
  --van-pagination-item-width: 20px;
  --van-pagination-background: transparent;
  // --van-pagination-item-default-color: var(--default-font-color);
}
</style>