Vue3.2 + Element-Plus 二次封装 el-table(TypeScript版🔥🔥)

19383

前言 📖

在公司经常接触到后台管理系统的开发,基本上 90% 以上都是 table 页面,业务逻辑也基本上一样。刚开始也想的是找一个基于 element 二次封装的 el-table,在搜寻过程中也接触了很多优秀的项目,其中包括:(vxe-table、avue……)但是这些项目终归没有自己开发灵活,所有就诞生了我的 Pro-Table 组件🎉🎉🎉 目前能节省我 60% 的工作量。(仅代表个人实践)

💢 请注意:以下内容只代表我个人封装思想,如果你觉得还不错,请帮我点个小小的 star,如果你有更好的想法,请在评论区留言,蟹蟹 😆😆

一、在线预览 👀

Link:admin.spicyboy.cn

二、Git 仓库地址 (欢迎 Star⭐⭐⭐)

Gitee:gitee.com/HalseySpicy…

GitHub:github.com/HalseySpicy…

三、Pro-Table 功能 🔨🔨🔨

  • 表格内容自适应屏幕宽高,溢出内容表格内部滚动。
  • 表格数据操作 Hooks (单条数据删除、批量删除、重置密码、状态切换……)
  • 表格数据多选 Hooks (支持现跨页勾选数据)
  • 表格序号、每行可自定义展开信息、表格头部自定义渲染(使用 tsx 语法)
  • 表格列排序、单元格内容格式化(有字典会根据字典自动格式化)
  • 树形表格展示(后期会增加懒加载)
  • 表格数据导入组件、导出钩子函数
  • 表格查询(可携带初始参数)、重置功能的封装
  • 表格分页模块封装(Pagination)
  • 表格数据刷新、列显隐、搜索显隐设置

四、需求分析 📑

首先我们来看效果图(总共可以分为五个区域):

table1.png

  • 1、表格搜索区域
  • 2、表格数据操作按钮区域
  • 3、表格功能按钮区域
  • 4、表格主体内容展示区域
  • 5、表格分页区域

1、搜索区域分析:

可以看到搜索区域的字段都是存在于表格当中的,并且每个页面的搜索、重置方法都是一样的逻辑,只是不同的查询参数而已。我们完全可以在传表格配置项 columns 时,直接指定某个字段的 search:true 就能把该项变为搜索项,然后使用 SearchType 字段可以指定搜索框的类型,最后把表格的搜索方法都封装成 Hooks 钩子函数。页面上完全就不会存在搜索逻辑了。

2、表格数据操作按钮区域分析:

表格数据操作按钮基本上每个页面都会不一样,所以我们直接使用 作用域插槽 来完成每个页面的数据操作按钮区域,作用域插槽 可以将表格多选数据信息从 Pro-TableHooks 多选钩子函数中传到页面上使用。

3、表格功能按钮区域分析:

这块区域没什么特殊功能,只有三个按钮,其功能分别为:表格数据刷新(一直会携带当前查询和分页条件)、表格列显隐设置、表格搜索区域显隐(方便展示更多的数据信息)。 可通过 toolButton 属性控制这块区域的显隐。

4、表格主体内容展示区域分析:

这块区域主要就是数据展示,配置 columns 项传到 Pro-Table 组件中就行了。使用作用域插槽可以自定义每一列的显示自己需要的内容,还支持表格数据多选(内部已封装了多选 Hooks 钩子函数)。

5、表格分页区域分析:

分页也没有什么特殊的功能,该支持的都支持了。 🤣🤣

五、Pro-Table 文档

1、Pro-Table 属性配置:

字段字段类型是否必传默认值字段描述
columnsColumnPropsPro-Table 会根据此字段渲染搜索表单与表格列
requestApiFunction获取表格数据的请求 API
dataCallbackFunction返回数据的回调函数,可以对数据进行处理
paginationBooleantrue是否显示分页组件
initParamObject{}是否携带表格请求的初始化参数
borderBooleantrue是否带有纵向边框
stripeBooleanfalse是否为斑马纹 table
toolButtonBooleantrue是否显示表格工具按钮区域
childrenNameStringchildren当表格为树形表格时,指定 children 字段名

2、ColumnProps 属性配置(都是可选参数):

字段字段类型默认值可选值字段描述
typeStringindex | selection | expand特殊类型(序号、多选、展开)
propString字段名称对应列内容的字段名
labelString表头标题
widthNumber | String单元格宽度
minWidthNumber | String单元格最小列宽
isShowBooleantrue是否显示在表格内
sortableBooleanfalse是否静态排序
fixedStringleft | right固定在表格左、右
tagBooleanfalse是否是标签展示
searchBooleanfalse是否为搜索项
searchTypeStringtexttext | select | multipleSelect | treeSelect | mutipleTreeSelect | date | timerange | datetimerange搜索项类型
searchPropsObject搜索项参数,根据 element 文档来,标签自带属性 > props 属性
searchInitParamString | Number | Boolean | Any[]搜索项是否带初始化参数
enumObject字典,可格式化单元格,还可以作为搜索框的下拉选项
renderHeaderFunction自定义表头

六、代码实现💪(详情去项目里查看,这里只贴了一部分代码)

使用一段话总结下我的想法:📚📚

把一个表格页面所有重复的功能 (表格多选、查询、重置、刷新、分页器、数据操作二次确认、文件下载、文件上传) 都封装成 Hooks 函数钩子,然后在 Pro-Table 组件中使用这些函数钩子。在页面中使用的时,只需传给 Pro-Table 当前表格数据的请求 API,表格配置项 columns 就行了,数据传输都使用作用域插槽从 Pro-Table 传给父组件就能在页面上获取到了。

1、常用 Hooks 函数

  • useTable:
import { Table } from "./interface";
import { reactive, computed, onMounted, toRefs } from "vue";

/**
 * @description table 页面操作方法封装
 * @param {Function} api 获取表格数据 api 方法(必传)
 * @param {Object} initParam 获取数据初始化参数(非必传,默认为{})
 * @param {Boolean} isPageable 是否有分页(非必传,默认为true)
 * @param {Function} dataCallBack 对后台返回的数据进行处理的方法(非必传)
 * */
export const useTable = (
  api: (params: any) => Promise<any>,
  initParam: object = {},
  isPageable: boolean = true,
  dataCallBack?: (data: any) => any
) => {
  const state = reactive<Table.TableStateProps>({
    // 表格数据
    tableData: [],
    // 分页数据
    pageable: {
      // 当前页数
      pageNum: 1,
      // 每页显示条数
      pageSize: 10,
      // 总条数
      total: 0
    },
    // 查询参数(只包括查询)
    searchParam: {},
    // 初始化默认的查询参数
    searchInitParam: {},
    // 总参数(包含分页和查询参数)
    totalParam: {}
  });

  /**
   * @description 分页查询参数(只包括分页和表格字段排序,其他排序方式可自行配置)
   * */
  const pageParam = computed({
    get: () => {
      return {
        pageNum: state.pageable.pageNum,
        pageSize: state.pageable.pageSize
      };
    },
    set: (newVal: any) => {
      console.log("我是分页更新之后的值", newVal);
    }
  });

  // 初始化的时候需要做的事情就是 设置表单查询默认值 && 获取表格数据(reset函数的作用刚好是这两个功能)
  onMounted(() => {
    reset();
  });

  /**
   * @description 获取表格数据
   * @return void
   * */
  const getTableList = async () => {
    try {
      // 先把初始化参数和分页参数放到总参数里面
      Object.assign(state.totalParam, initParam, isPageable ? pageParam.value : {});
      let { data } = await api(state.totalParam);
      dataCallBack && (data = dataCallBack(data));
      state.tableData = isPageable ? data.datalist : data;
      // 解构后台返回的分页数据 (如果有分页更新分页信息)
      const { pageNum, pageSize, total } = data;
      isPageable && updatePageable({ pageNum, pageSize, total });
    } catch (error) {
      console.log(error);
    }
  };

  /**
   * @description 更新查询参数
   * @return void
   * */
  const updatedTotalParam = () => {
    state.totalParam = {};
    // 处理查询参数,可以给查询参数加自定义前缀操作
    let nowSearchParam: { [key: string]: any } = {};
    // 防止手动清空输入框携带参数(这里可以自定义查询参数前缀)
    for (let key in state.searchParam) {
      // * 某些情况下参数为 false/0 也应该携带参数
      if (state.searchParam[key] || state.searchParam[key] === false || state.searchParam[key] === 0) {
        nowSearchParam[key] = state.searchParam[key];
      }
    }
    Object.assign(state.totalParam, nowSearchParam, isPageable ? pageParam.value : {});
  };

  /**
   * @description 更新分页信息
   * @param {Object} resPageable 后台返回的分页数据
   * @return void
   * */
  const updatePageable = (resPageable: Table.Pageable) => {
    Object.assign(state.pageable, resPageable);
  };

  /**
   * @description 表格数据查询
   * @return void
   * */
  const search = () => {
    state.pageable.pageNum = 1;
    updatedTotalParam();
    getTableList();
  };

  /**
   * @description 表格数据重置
   * @return void
   * */
  const reset = () => {
    state.pageable.pageNum = 1;
    state.searchParam = {};
    // 重置搜索表单的时,如果有默认搜索参数,则重置默认的搜索参数
    Object.keys(state.searchInitParam).forEach(key => {
      state.searchParam[key] = state.searchInitParam[key];
    });
    updatedTotalParam();
    getTableList();
  };

  /**
   * @description 每页条数改变
   * @param {Number} val 当前条数
   * @return void
   * */
  const handleSizeChange = (val: number) => {
    state.pageable.pageNum = 1;
    state.pageable.pageSize = val;
    getTableList();
  };

  /**
   * @description 当前页改变
   * @param {Number} val 当前页
   * @return void
   * */
  const handleCurrentChange = (val: number) => {
    state.pageable.pageNum = val;
    getTableList();
  };

  return {
    ...toRefs(state),
    getTableList,
    search,
    reset,
    handleSizeChange,
    handleCurrentChange
  };
};

  • useSelection:
import { ref, computed } from "vue";

/**
 * @description 表格多选数据操作
 * */
export const useSelection = () => {
  // 是否选中数据
  const isSelected = ref<boolean>(false);
  // 选中的数据列表
  const selectedList = ref([]);

  // 当前选中的所有ids(数组),可根据项目自行配置id字段
  const selectedListIds = computed((): string[] => {
    let ids: string[] = [];
    selectedList.value.forEach(item => {
      ids.push(item["id"]);
    });
    return ids;
  });

  // 获取行数据的 Key,用来优化 Table 的渲染;在使用跨页多选时,该属性是必填的
  const getRowKeys = (row: { id: string }) => {
    return row.id;
  };

  /**
   * @description 多选操作
   * @param {Array} rowArr 当前选择的所有数据
   * @return void
   */
  const selectionChange = (rowArr: any) => {
    rowArr.length === 0 ? (isSelected.value = false) : (isSelected.value = true);
    selectedList.value = rowArr;
  };

  return {
    isSelected,
    selectedList,
    selectedListIds,
    selectionChange,
    getRowKeys
  };
};

  • useDownload:
import { ElNotification } from "element-plus";

/**
 * @description 接收数据流生成blob,创建链接,下载文件
 * @param {Function} api 导出表格的api方法(必传)
 * @param {String} tempName 导出的文件名(必传)
 * @param {Object} params 导出的参数(默认为空对象)
 * @param {Boolean} isNotify 是否有导出消息提示(默认为 true)
 * @param {String} fileType 导出的文件格式(默认为.xlsx)
 * @return void
 * */
export const useDownload = async (
  api: (param: any) => Promise<any>,
  tempName: string,
  params: any = {},
  isNotify: boolean = true,
  fileType: string = ".xlsx"
) => {
  if (isNotify) {
    ElNotification({
      title: "温馨提示",
      message: "如果数据庞大会导致下载缓慢哦,请您耐心等待!",
      type: "info",
      duration: 3000
    });
  }
  try {
    const res = await api(params);
    // 这个地方的type,经测试不传也没事,因为zip文件不知道type是什么
    // const blob = new Blob([res], {
    // 	type: "application/vnd.ms-excel;charset=UTF-8"
    // });
    const blob = new Blob([res]);
    // 兼容edge不支持createObjectURL方法
    if ("msSaveOrOpenBlob" in navigator) return window.navigator.msSaveOrOpenBlob(blob, tempName + fileType);
    const blobUrl = window.URL.createObjectURL(blob);
    const exportFile = document.createElement("a");
    exportFile.style.display = "none";
    exportFile.download = `${tempName}${fileType}`;
    exportFile.href = blobUrl;
    document.body.appendChild(exportFile);
    exportFile.click();
    // 去除下载对url的影响
    document.body.removeChild(exportFile);
    window.URL.revokeObjectURL(blobUrl);
  } catch (error) {
    console.log(error);
  }
};

  • useHandleData:
import { ElMessageBox, ElMessage } from "element-plus";
import { HandleData } from "./interface";

/**
 * @description 操作单条数据信息(二次确认【删除、禁用、启用、重置密码】)
 * @param {Function} api 操作数据接口的api方法(必传)
 * @param {Object} params 携带的操作数据参数 {id,params}(必传)
 * @param {String} message 提示信息(必传)
 * @param {String} confirmType icon类型(不必传,默认为 warning)
 * @return Promise
 */
export const useHandleData = (
  api: (params: any) => Promise<any>,
  params: Parameters<typeof api>[0],
  message: string,
  confirmType: HandleData.MessageType = "warning"
) => {
  return new Promise((resolve, reject) => {
    ElMessageBox.confirm(`是否${message}?`, "温馨提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: confirmType,
      draggable: true
    }).then(async () => {
      const res = await api(params);
      if (!res) return reject(false);
      ElMessage({
        type: "success",
        message: `${message}成功!`
      });
      resolve(true);
    });
  });
};

2、Pro-table 组件:

  • Template
<!-- 💢💢💢 后期会重构 Pro-Table 组件,使用 v-bind 属性透传 -->

<template>
  <div class="table-box">
    <!-- 查询表单 -->
    <SearchForm :search="search" :reset="reset" :searchParam="searchParam" :columns="searchColumns" v-show="isShowSearch" />
    <!-- 表格头部 操作按钮 -->
    <div class="table-header">
      <div class="header-button-lf">
        <slot name="tableHeader" :ids="selectedListIds" :isSelected="isSelected"></slot>
      </div>
      <div class="header-button-ri" v-if="toolButton">
        <el-button :icon="Refresh" circle @click="getTableList"> </el-button>
        <el-button :icon="Operation" circle @click="openColSetting"> </el-button>
        <el-button :icon="Search" circle v-if="searchColumns.length" @click="isShowSearch = !isShowSearch"> </el-button>
      </div>
    </div>
    <!-- 表格主体 -->
    <el-table
      height="575"
      ref="tableRef"
      :data="tableData"
      :border="border"
      @selection-change="selectionChange"
      :row-key="getRowKeys"
      :stripe="stripe"
      :tree-props="{ children: childrenName }"
    >
      <template v-for="item in tableColumns" :key="item">
        <!-- selection || index -->
        <el-table-column
          v-if="item.type == 'selection' || item.type == 'index'"
          :type="item.type"
          :reserve-selection="item.type == 'selection'"
          :label="item.label"
          :width="item.width"
          :min-width="item.minWidth"
          :fixed="item.fixed"
        >
        </el-table-column>
        <!-- expand(展开查看详情,请使用作用域插槽) -->
        <el-table-column
          v-if="item.type == 'expand'"
          :type="item.type"
          :label="item.label"
          :width="item.width"
          :min-width="item.minWidth"
          :fixed="item.fixed"
          v-slot="scope"
        >
          <slot :name="item.type" :row="scope.row"></slot>
        </el-table-column>
        <!-- other -->
        <el-table-column
          v-if="!item.type && item.prop && item.isShow"
          :prop="item.prop"
          :label="item.label"
          :width="item.width"
          :min-width="item.minWidth"
          :sortable="item.sortable"
          :show-overflow-tooltip="item.prop !== 'operation'"
          :resizable="true"
          :fixed="item.fixed"
        >
          <!-- 自定义 header (使用组件渲染 tsx 语法) -->
          <template #header v-if="item.renderHeader">
            <component :is="item.renderHeader" :row="item"> </component>
          </template>

          <!-- 自定义配置每一列 slot(使用作用域插槽) -->
          <template #default="scope">
            <slot :name="item.prop" :row="scope.row">
              <!-- tag 标签(自带格式化内容) -->
              <el-tag v-if="item.tag" :type="filterEnum(scope.row[item.prop!], item.enum!, item.searchProps,'tag')">
                {{
		    item.enum?.length ? filterEnum(scope.row[item.prop!], item.enum!, item.searchProps) : formatValue(scope.row[item.prop!])
                }}
              </el-tag>
              <!-- 文字(自带格式化内容) -->
              <span v-else>
                {{
		    item.enum?.length ? filterEnum(scope.row[item.prop!], item.enum!, item.searchProps) : formatValue(scope.row[item.prop!])
                }}
              </span>
            </slot>
          </template>
        </el-table-column>
      </template>
      <template #empty>
        <div class="table-empty">
          <img src="@/assets/images/notData.png" alt="notData" />
          <div>暂无数据</div>
        </div>
      </template>
    </el-table>
    <!-- 分页 -->
    <Pagination
      v-if="pagination"
      :pageable="pageable"
      :handleSizeChange="handleSizeChange"
      :handleCurrentChange="handleCurrentChange"
    />
    <!-- 列设置 -->
    <ColSetting v-if="toolButton" ref="colRef" :tableRef="tableRef" :colSetting="colSetting" />
  </div>
</template>
  • Script
<script setup lang="ts" name="proTable">
import { ref, watch } from "vue";
import { useTable } from "@/hooks/useTable";
import { useSelection } from "@/hooks/useSelection";
import { Refresh, Operation, Search } from "@element-plus/icons-vue";
import { ColumnProps } from "@/components/ProTable/interface";
import { filterEnum, formatValue } from "@/utils/util";
import SearchForm from "@/components/SearchForm/index.vue";
import Pagination from "./components/Pagination.vue";
import ColSetting from "./components/ColSetting.vue";

// 表格 DOM 元素
const tableRef = ref();

// 是否显示搜索模块
const isShowSearch = ref<boolean>(true);

interface ProTableProps {
  columns: Partial<ColumnProps>[]; // 列配置项
  requestApi: (params: any) => Promise<any>; // 请求表格数据的api ==> 必传
  dataCallback?: (data: any) => any; // 返回数据的回调函数,可以对数据进行处理
  pagination?: boolean; // 是否需要分页组件 ==> 非必传(默认为true)
  initParam?: any; // 初始化请求参数 ==> 非必传(默认为{})
  border?: boolean; // 表格是否显示边框 ==> 非必传(默认为true)
  stripe?: boolean; // 是否带斑马纹表格 ==> 非必传(默认为false)
  toolButton?: boolean; // 是否显示表格功能按钮 ==> 非必传(默认为true)
  childrenName?: string; // 当数据存在 children 时,指定 children key 名字 ==> 非必传(默认为"children")
}

// 接受父组件参数,配置默认值
const props = withDefaults(defineProps<ProTableProps>(), {
  columns: () => [],
  pagination: true,
  initParam: {},
  border: true,
  stripe: false,
  toolButton: true,
  childrenName: "children"
});

// 表格多选 Hooks
const { selectionChange, getRowKeys, selectedListIds, isSelected } = useSelection();

// 表格操作 Hooks
const { tableData, pageable, searchParam, searchInitParam, getTableList, search, reset, handleSizeChange, handleCurrentChange } =
  useTable(props.requestApi, props.initParam, props.pagination, props.dataCallback);

// 监听页面 initParam 改化,重新获取表格数据
watch(
  () => props.initParam,
  () => {
    getTableList();
  },
  { deep: true }
);

// 表格列配置项处理(添加 isShow 属性,控制显示/隐藏)
const tableColumns = ref<Partial<ColumnProps>[]>();
tableColumns.value = props.columns.map(item => {
  return {
    ...item,
    isShow: item.isShow ?? true
  };
});

// 如果当前 enum 为后台数据需要请求数据,则调用该请求接口,获取enum数据
tableColumns.value.forEach(async item => {
  if (item.enum && typeof item.enum === "function") {
    const { data } = await item.enum();
    item.enum = data;
  }
});

// 过滤需要搜索的配置项
const searchColumns = tableColumns.value.filter(item => item.search);
// 设置搜索表单的默认值
searchColumns.forEach(column => {
  if (column.searchInitParam !== undefined && column.searchInitParam !== null) {
    searchInitParam.value[column.prop!] = column.searchInitParam;
  }
});

// * 列设置
const colRef = ref();
// 过滤掉不需要设置显隐的列
const colSetting = tableColumns.value.filter((item: Partial<ColumnProps>) => {
  return (
    item.type !== "selection" &&
    item.type !== "index" &&
    item.type !== "expand" &&
    item.prop !== "operation" &&
    item.isShow !== false
  );
});
const openColSetting = () => {
  colRef.value.openColSetting();
};

// 暴露给父组件的参数和方法
defineExpose({ searchParam, refresh: getTableList });
</script>

3、页面使用:

<template>
  <div class="table-box">
    <ProTable ref="proTable" :columns="columns" :requestApi="getUserList" :initParam="initParam" :dataCallback="dataCallback">
      <!-- 表格 header 按钮 -->
      <template #tableHeader="scope">
        <el-button type="primary" :icon="CirclePlus" @click="openDrawer('新增')" v-if="BUTTONS.add">新增用户</el-button>
        <el-button type="primary" :icon="Upload" plain @click="batchAdd" v-if="BUTTONS.batchAdd">批量添加用户</el-button>
        <el-button type="primary" :icon="Download" plain @click="downloadFile" v-if="BUTTONS.export">导出用户数据</el-button>
        <el-button
          type="danger"
          :icon="Delete"
          plain
          :disabled="!scope.isSelected"
          @click="batchDelete(scope.ids)"
          v-if="BUTTONS.batchDelete"
        >
          批量删除用户
        </el-button>
      </template>
      <!-- Expand -->
      <template #expand="scope">
        {{ scope.row }}
      </template>
      <!-- 用户状态 slot -->
      <template #status="scope">
        <!-- 如果插槽的值为 el-switch,第一次加载会默认触发 switch 的 @change 方法,所有在外层包一个盒子,点击触发盒子 click 方法(暂时只能这样解决) -->
        <div @click="changeStatus(scope.row)" v-if="BUTTONS.status">
          <el-switch
            :value="scope.row.status"
            :active-text="scope.row.status === 1 ? '启用' : '禁用'"
            :active-value="1"
            :inactive-value="0"
          />
        </div>
        <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" v-else>
          {{ scope.row.status === 1 ? "启用" : "禁用" }}
        </el-tag>
      </template>
      <!-- 表格操作 -->
      <template #operation="scope">
        <el-button type="primary" link :icon="View" @click="openDrawer('查看', scope.row)">查看</el-button>
        <el-button type="primary" link :icon="EditPen" @click="openDrawer('编辑', scope.row)">编辑</el-button>
        <el-button type="primary" link :icon="Refresh" @click="resetPass(scope.row)">重置密码</el-button>
        <el-button type="primary" link :icon="Delete" @click="deleteAccount(scope.row)">删除</el-button>
      </template>
    </ProTable>
    <UserDrawer ref="drawerRef" />
    <ImportExcel ref="dialogRef" />
  </div>
</template>

<script setup lang="tsx" name="useComponent">
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { User } from "@/api/interface";
import { ColumnProps } from "@/components/ProTable/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { useDownload } from "@/hooks/useDownload";
import { useAuthButtons } from "@/hooks/useAuthButtons";
import ProTable from "@/components/ProTable/index.vue";
import ImportExcel from "@/components/ImportExcel/index.vue";
import UserDrawer from "@/views/proTable/components/UserDrawer.vue";
import { CirclePlus, Delete, EditPen, Download, Upload, View, Refresh } from "@element-plus/icons-vue";
import {
  getUserList,
  deleteUser,
  editUser,
  addUser,
  changeUserStatus,
  resetUserPassWord,
  exportUserInfo,
  BatchAddUser,
  getUserStatus,
  getUserGender
} from "@/api/modules/user";

// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref();

// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive({
  type: 1
});

// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 datalist && total && pageNum && pageSize 这些字段,那么你可以在这里进行处理成这些字段
const dataCallback = (data: any) => {
  return {
    datalist: data.datalist,
    total: data.total,
    pageNum: data.pageNum,
    pageSize: data.pageSize
  };
};

// 页面按钮权限
const { BUTTONS } = useAuthButtons();

// 自定义渲染头部(使用tsx语法)
const renderHeader = (scope: any) => {
  return (
    <el-button
      type="primary"
      onClick={() => {
        ElMessage.success("我是自定义表头");
      }}
    >
      {scope.row.label}
    </el-button>
  );
};

// 表格配置项
const columns: Partial<ColumnProps>[] = [
  { type: "selection", width: 80, fixed: "left" },
  { type: "index", label: "#", width: 80 },
  { type: "expand", label: "Expand", width: 100 },
  { prop: "username", label: "用户姓名", width: 130, search: true, searchProps: { disabled: true }, renderHeader },
  // 😄 enum 可以直接是数组对象,也可以是请求方法(proTable 内部会执行获取 enum 的这个方法),下面用户状态也同理
  // 😄 enum 为请求方法时,后台返回的数组对象 key 值不是 label 和 value 的情况,可以在 searchProps 中指定 label 和 value 的 key 值
  {
    prop: "gender",
    label: "性别",
    width: 120,
    sortable: true,
    search: true,
    searchType: "select",
    enum: getUserGender,
    searchProps: { label: "genderLabel", value: "genderValue" }
  },
  { prop: "idCard", label: "身份证号", search: true },
  { prop: "email", label: "邮箱", search: true },
  { prop: "address", label: "居住地址", search: true },
  {
    prop: "status",
    label: "用户状态",
    sortable: true,
    search: true,
    searchType: "select",
    enum: getUserStatus,
    searchProps: { label: "userLabel", value: "userStatus" }
  },
  {
    prop: "createTime",
    label: "创建时间",
    width: 200,
    sortable: true,
    search: true,
    searchType: "datetimerange",
    searchProps: {
      disabledDate: (time: Date) => time.getTime() < Date.now() - 8.64e7
    },
    searchInitParam: ["2022-08-30 00:00:00", "2022-08-20 23:59:59"]
  },
  { prop: "operation", label: "操作", width: 330, fixed: "right", renderHeader }
];

// 删除用户信息
const deleteAccount = async (params: User.ResUserList) => {
  await useHandleData(deleteUser, { id: [params.id] }, `删除【${params.username}】用户`);
  proTable.value.refresh();
};

// 批量删除用户信息
const batchDelete = async (id: string[]) => {
  await useHandleData(deleteUser, { id }, "删除所选用户信息");
  proTable.value.refresh();
};

// 重置用户密码
const resetPass = async (params: User.ResUserList) => {
  await useHandleData(resetUserPassWord, { id: params.id }, `重置【${params.username}】用户密码`);
  proTable.value.refresh();
};

// 切换用户状态
const changeStatus = async (row: User.ResUserList) => {
  await useHandleData(changeUserStatus, { id: row.id, status: row.status == 1 ? 0 : 1 }, `切换【${row.username}】用户状态`);
  proTable.value.refresh();
};

// 导出用户列表
const downloadFile = async () => {
  useDownload(exportUserInfo, "用户列表", proTable.value.searchParam);
};

// 批量添加用户
interface DialogExpose {
  acceptParams: (params: any) => void;
}
const dialogRef = ref<DialogExpose>();
const batchAdd = () => {
  let params = {
    title: "用户",
    tempApi: exportUserInfo,
    importApi: BatchAddUser,
    getTableList: proTable.value.refresh
  };
  dialogRef.value!.acceptParams(params);
};

// 打开 drawer(新增、查看、编辑)
interface DrawerExpose {
  acceptParams: (params: any) => void;
}
const drawerRef = ref<DrawerExpose>();
const openDrawer = (title: string, rowData: Partial<User.ResUserList> = { avatar: "" }) => {
  let params = {
    title,
    rowData: { ...rowData },
    isView: title === "查看",
    apiUrl: title === "新增" ? addUser : title === "编辑" ? editUser : "",
    getTableList: proTable.value.refresh
  };
  drawerRef.value!.acceptParams(params);
};
</script>