添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Repository files navigation

pro-table

基于 element-plus 二次封装的通用表格组件

将页面分成五个部分

🕶 ProTable 组件内部通过 属性透传 实现父子组件传值,所以支持 el-table && el-table-column 所有属性、事件、方法的调用,不会有任何心智负担。

  • 表格搜索区域使用 Grid 布局,支持自定义响应式配置
  • 表格搜索、重置、分页查询、刷新等 hooks 封装(分离状态逻辑和UI逻辑)
  • 表格数据多选 hooks 封装 (支持现跨页勾选数据)
  • 表格数据刷新、列显隐、列排序、搜索区域显隐设置
  • 支持多级表头、表头内容自定义渲染(支持作用域插槽、tsx 语法)
  • 支持单元格内容自定义渲染(支持作用域插槽、tsx 语法)
  • 支持 el-table && el-table-column 所有属性、事件、方法的调用
  • Search 配置

    目前支持的组件:

    el-input, el-input-number, el-select, el-select-v2, el-tree-select, el-cascader,

    el-date-picker, el-time-picker, el-switch, el-slider

    当前项搜索框的类型,支持:input、input-number、select、select-v2、tree-select、cascader、date-packer、time-picker、time-select、switch、slider props Object 根据 element plus 官方文档来传递,该属性所有值会透传到组件 defaultValue 搜索项默认值 String 当搜索项 key 不为 prop 属性时,可通过 key 指定 order Number 搜索项排序(从小到大) Number 搜索项所占用的列数,默认为 1 列 offset Number 搜索字段左侧偏移列数 render Function 自定义搜索内容渲染(tsx 语法、h 语法)

    ProTable 组件内部通过 属性透传 实现父子组件传值,所以支持 el-table && el-table-column 所有事件的调用

    对于 el-table DOM,可通过 proTable.value.element.方法名 调用其方法

    el-table

    对于 proTable 的方法,可以通过绑定 ref 实现,调用 proTable.value?.selectedList

    <script setup lang="tsx" name="useProTable">
    import ProTable from '@/components/ProTable/index.vue';
    import { getUserList } from '@/api/index'
    import { ColumnProps, HeaderRenderScope, ProTableInstance } from './components/ProTable/interface';
    import { ElMessage } from 'element-plus';
    import { CirclePlus, Delete, EditPen, View, Refresh } from "@element-plus/icons-vue";
    import { ref, reactive, computed } from 'vue';
    interface ResUserList {
      id: string;
      username: string;
      gender: number;
      user: { detail: { age: number } };
      idCard: string;
      email: string;
      address: string;
      createTime: string;
      status: number;
      avatar: string;
      photo: any[];
      children?: ResUserList[];
    const proTable = ref<ProTableInstance>();
    const initParam = reactive({ type: 1 });
    const headerRender = (scope: HeaderRenderScope<ResUserList>) => {
      return (
        <el-button type="primary" onClick={() => ElMessage.success("我是通过 tsx 语法渲染的表头")}>
          {scope.column.label}
        </el-button>
    const selected = computed(() => proTable.value?.selectedList)
    console.log('Protable暴露出来的属性和方法', proTable)
    console.log('勾选的数据', selected)
    const dataCallback = (data: any) => {
      const result =  {
        list: data.data.list,
        total: data.data.total,
        pageNum: data.data.pageNum,
        pageSize: data.data.pageSize
      console.log('data', result)
      return result
    const getTableList = (params: any) => {
      let newParams = JSON.parse(JSON.stringify(params));
      newParams.createTime && (newParams.startTime = newParams.createTime[0]);
      newParams.createTime && (newParams.endTime = newParams.createTime[1]);
      delete newParams.createTime;
      return getUserList(newParams);
    // 表格配置项
    const columns: ColumnProps<ResUserList>[] = [
      { type: "selection", fixed: "left", width: 50 },
      { type: "index", label: "#", width: 50 },
      { type: "expand", label: "Expand", width: 100 },
        prop: "username",
        label: "用户姓名",
        search: { el: "input" },
        render: scope => {
          // 自定义渲染内容
          return (
            <el-button type="primary" link onClick={() => ElMessage.success("我是通过 tsx 语法渲染的内容")}>
              {scope.row.username}
            </el-button>
        prop: "gender",
        label: "性别",
        // 字典数据
        // enum: genderType,
        // 字典请求不带参数
        enum: [{genderLabel: "", genderValue: 1}, {genderLabel: "", genderValue: 2}],
        // 字典请求携带参数
        // enum: () => getUserGender({ id: 1 }),
        search: { el: "select", props: { filterable: true } },
        fieldNames: { label: "genderLabel", value: "genderValue" }
        // 多级 prop
        prop: "user.detail.age",
        label: "年龄",
        search: {
          // 自定义 search 显示内容
          render: ({ searchParam }) => {
            return (
              <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
                <el-input vModel_trim={searchParam.minAge} placeholder="最小年龄" />
                <span style={{ marginRight: '10px', marginLeft: '10px' }}>-</span>
                <el-input vModel_trim={searchParam.maxAge} placeholder="最大年龄" />
              </div>
      { prop: "idCard", label: "身份证号", search: { el: "input" } },
      { prop: "email", label: "邮箱" },
      { prop: "address", label: "居住地址" },
        prop: "status",
        label: "用户状态",
        enum: [{userLabel: "启用", userStatus: 1}, {userLabel: "禁用", userStatus: 0}],
        search: { el: "tree-select", props: { filterable: true } },
        fieldNames: { label: "userLabel", value: "userStatus" },
        render: scope => {
          // 自定义渲染内容
          return (
            {/* 这里是使用了自定义指令实现权限校验,这里省略 */}
              {true ? (
                <el-switch
                  model-value={scope.row.status}
                  active-text={scope.row.status ? "启用" : "禁用"}
                  active-value={1}
                  inactive-value={0}
                  onClick={() => changeStatus(scope.row)}
              ) : (
                <el-tag type={scope.row.status ? "success" : "danger"}>{scope.row.status ? "启用" : "禁用"}</el-tag>
        prop: "createTime",
        label: "创建时间",
        headerRender,
        width: 180,
        search: {
          el: "date-picker",
          span: 2,
          props: { type: "datetimerange", valueFormat: "YYYY-MM-DD HH:mm:ss" },
          defaultValue: ["2022-11-12 11:35:00", "2022-12-12 11:35:00"]
      { prop: "operation", label: "操作", fixed: "right", width: 330 }
    //改变用户状态: 启用/禁用
    const changeStatus = (row: any) => {
      ElMessage.success(`row:` + row)
    const addUser = () => {
      ElMessage.success('新增用户')
    const deleteUser = (row: any) => {
      ElMessage.success('删除用户' + row)
    const openDrawer = (option: any,row: any) => {
      ElMessage.success(option + row)
    const resetPass = (row: any) => {
      ElMessage.success('重置密码' + row)
    const deleteAccount = (row: any) => {
      ElMessage.success('删除' + row)
    </script>
    <template>
    <div class="content-box">
      <ProTable
        ref="proTable"
        title="用户列表"
        :columns="columns"
        :request-api="getTableList"
        :init-param="initParam"
        :data-callback="dataCallback"
        <!-- 表格 header 按钮 -->
        <template #tableHeader="scope">
          <el-button type="primary" :icon="CirclePlus" @click="addUser()"> 新增用户 </el-button>
          <el-button type="danger" :icon="Delete" plain :disabled="!scope.isSelected" @click="deleteUser(scope.selectedListIds)">
          </el-button>
        </template>
        <!-- Expand -->
        <template #expand="scope">
          {{ scope.row }}
        </template>
        <!-- usernameHeader -->
        <template #usernameHeader="scope">
          <el-button type="primary" @click="ElMessage.success('我是通过作用域插槽渲染的表头')">
            {{ scope.column.label }}
          </el-button>
        </template>
        <!-- createTime -->
        <template #createTime="scope">
          <el-button type="primary" link @click="ElMessage.success('我是通过作用域插槽渲染的内容')">
            {{ scope.row.createTime }}
          </el-button>
        </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>
      <div>{{ selected }}</div>
    </div>
    </template>

    🧐 首先我们在封装 ProTable 组件的时候,在不影响 el-table 原有的属性、事件、方法的前提下,然后在其基础上做二次封装,否则做得再好,也不太完美。

    表格搜索区域

    搜索区域的字段都是存在于表格中的,并且搜索、重置逻辑可复用,所以抽离成 hook,当我们给 pro-table 组件传递 columns 数组时,对于每一个 column(即每一个字段)设置一个 search 属性配置,就能把该项变为搜索项,在 search 配置中可以通过 el 字段指定搜索框的类型。

    不同类型的搜索框组件通过 component :is 动态组件实现

    表格数据操作按钮区域

    使用 作用域插槽 来完成每个页面的数据操作按钮区域

    scope 数据中包含: selectedList(当前选择的数据)、selectedListIds(当前选择的数据id)、isSelected(当前是否选中的数据)

    <!-- ProTable 中 tableHeader 插槽 -->
    <slot name="tableHeader" :selectList="selectedList" :selectedListIds="selectedListIds" :isSelected="isSelected"></slot>
    <!-- 页面使用 -->
    <template #tableHeader="scope">
        <el-button type="primary" :icon="CirclePlus" @click="openDrawer('新增')">新增用户</el-button>
        <el-button type="primary" :icon="Upload" plain @click="batchAdd">批量添加用户</el-button>
        <el-button type="primary" :icon="Download" plain @click="downloadFile">导出用户数据</el-button>
        <el-button type="danger" :icon="Delete" plain @click="batchDelete(scope.selectedListIds)" :disabled="!scope.isSelected">批量删除用户</el-button>
    </template>

    关于跨页勾选数据

    <el-table :row-key='rowkey'>
    	<el-table-column :reserve-selection="item.type == 'selection'"></el-table-column>
    </el-table>
    

    表格功能按钮区域

    控制表格列设置(列显隐、列排序):

    在父组件中将 colSettingData 数据通过 v-model 绑定到 <colSetting /> 组件上,在该组件内部,通过 v-model绑定 isShowsortable属性,当通过 el-switch切换时,通过 v-model 实现视图驱动模型。

    表格主题内容展示区域

    可通过 作用域插槽 和 tsx 实现表头、单元格内容自定义渲染

    表头自定义渲染

    <template #usernameHeader="scope">
    	<el-button type="primary" @click="ElMessage.success('我是通过作用域插槽渲染的表头')">
    		{{ scope.column.label }}
    	</el-button>
    </template>
    // 自定义渲染表头(使用tsx语法)
    const headerRender = (scope: HeaderRenderScope<ResUserList>) => {
      return (
        <el-button type="primary" onClick={() => ElMessage.success("我是通过 tsx 语法渲染的表头")}>
          {scope.column.label}
        </el-button>
    const columns = [
    		prop: "createTime",
        label: "创建时间",
        headerRender,
        width: 180
    

    单元格自定义渲染

    <template #createTime="scope">
       <el-button type="primary" link @click="ElMessage.success('我是通过作用域插槽渲染的内容')">
          {{ scope.row.createTime }}
       </el-button>
     </template>
     // tsx语法
    const columns = [
        prop: "age",
        label: "年龄",
        render: scope => {
          // 自定义渲染内容
          return (
              <el-switch
                  model-value={scope.row.status}
                  active-text={scope.row.status ? "启用" : "禁用"}
                  active-value={1}
                  inactive-value={0}
                  onClick={() => changeStatus(scope.row)}
    

    暴露 el-table 方法和属性

    <template>
        <el-table
          ref="tableRef"
          v-bind="$attrs" 
        </el-table>
    </template>
    <script setup lang="ts" name="ProTable">
    import { ref } from "vue";
    import { ElTable } from "element-plus";
    const tableRef = ref<InstanceType<typeof ElTable>>();
    defineExpose({ element: tableRef });
    </script>