Vue3 + TSX 封装 el-table:还原 Antd 风格的 Columns 配置

背景

vue3 项目中使用的是 element-plustable 组件,页面中常用的功能,大概包含 排序多选 以及 列的自定义展示,所以很有封装的必要,否则每次都要写很长一串。

由于 table-column 是靠插槽自定义的列内容,总要嵌套一些 v-if 的逻辑,写法并不简洁。

传统写法如下:

arduino 复制代码
<template #default="{ row }">
  <template v-if="column.prop === 'email'">
    {{ `我的邮件:${row.email}` }}
  </template>
  ......
</template>

想法

习惯了 antd 中的表格,每次配置一个 columns,支持 render 函数式渲染,这样就将数据和 html 分离开了,不需要在模板中进行展示判断,只要在对应的数据位置,写自己的 render 函数就可以了。

Vue 中能不能像这样写 jsx 呢?

实现

  1. 确认安装了 @vitejs/plugin-vue-jsx 插件,并在 vite.config.ts 中进行了使用。
  2. 实现 BaseTable.tsx 组件。
typescript 复制代码
// BaseTable.tsx
import { defineComponent, PropType, h, Fragment } from "vue";
import { ElTable, ElTableColumn, ElPagination, ElLoading } from "element-plus";

type LabelType = string | number | (() => any);

interface Column {
  type?: "default" |"selection" | "index" | "expand"; // 列类型
  label: LabelType;
  key: string; // 对应数据字段 (传给 el-table-column 的 prop)
  render?: (row: any, column?: Column, index?: number) => any; // 返回 JSX 或字符串
  sortable?: boolean | "custom";
  width?: string | number;
  align?: "left" | "center" | "right";
  fixed?: boolean | "left" | "right";
  showOverflowTooltip?: boolean;
}

export default defineComponent({
  name: "BaseTable",
  props: {
    // --------- table 配置 ---------
    // 数据
    data: { type: Array as PropType<any[]>, required: true },
    // 列定义
    columns: { type: Array as PropType<Column[]>, required: true },
    // loading 状态
    loading: { type: Boolean, default: false },
    // 表格配置
    // stripe: 是否斑马纹
    stripe: { type: Boolean, default: false },
    // border: 是否带边框
    border: { type: Boolean, default: true },
    // height: 表格高度,支持字符串或数字
    height: {
      type: [String, Number] as PropType<string | number>,
      default: undefined,
    },
    // rowKey: 行唯一标识,支持字符串或函数
    rowKey: {
      type: [String, Function] as PropType<string | ((row: any) => string)>,
      default: undefined,
    },
    // --------- 下面为分页配置 ---------
    // showPagination: 是否显示分页器
    showPagination: {
      type: Boolean,
      default: true,
    },
    // total: 数据总条数
    total: {
      type: Number,
      default: 0,
    },
    // 分页器布局
    pageLayout: {
      type: String,
      default: "total, sizes, prev, pager, next, jumper"
    },
    // 分页器每页条数选项
    pageSizeOptions: {
      type: Array as PropType<number[]>,
      default: () => [10, 20, 30, 50],
    },
    // 每页条数
    pageSize: {
      type: Number,
      default: 10,
    },
    // 当前页码
    currentPage: {
      type: Number,
      default: 1,
    }
  },
  emits: ["sort-change", "selection-change", "page-change", "update:currentPage", "update:pageSize"],
  setup(props, { emit }) {
    const tableRef = ref<any>(null);
    const loadingInstance = ref<any>(null);
    // 事件转发给父组件
    const onSortChange = (sort: any) => emit("sort-change", sort);
    const onSelectionChange = (rows: any[]) => emit("selection-change", rows);
    const onPageChange = (page: number, size: number) => {
      // 更新当前页和每页条数,触发回调
      emit("update:currentPage", page);
      emit("update:pageSize", size);
      emit("page-change", page, size)
    };

    // 监听 loading
    watch(
      () => props.loading,
      (val) => {
        if (!tableRef.value) return;
        const tableEl = tableRef.value.$el as HTMLElement;
        if (val) {
          loadingInstance.value = ElLoading.service({
            target: tableEl,
            // text: '加载中...',
            // background: 'rgba(255,255,255,0.7)',
          });
        } else {
          loadingInstance.value?.close();
          loadingInstance.value = null;
        }
      },
      { immediate: true }
    );

    onUnmounted(() => {
      loadingInstance.value?.close();
    })

    return () => (
      <Fragment>
        <ElTable
          ref={tableRef}
          data={props.data}
          stripe={props.stripe}
          border={props.border}
          height={props.height}
          rowKey={props.rowKey as any}
          style={{width: "100%"}}
          onSortChange={onSortChange}
          onSelectionChange={onSelectionChange}
        >
          {props.columns.map((col, idx) => {
            // 处理 label:支持 string/number 或函数返回值(VNode/string)
            const header =
              typeof col.label === "function" ? col.label() : col.label;

            return (
              <ElTableColumn
                key={col.key ?? `col-${idx}` }
                prop={col.key}
                type={col.type || "default"}
                label={header as any}
                sortable={col.sortable}
                width={col.width}
                align={col.align || "center"}
                fixed={col.fixed as any}
                showOverflowTooltip={col.showOverflowTooltip}
              >
                {{
                  // 默认插槽拿到作用域 { row, column, $index }
                  default: ({ row, column, $index }: any) =>
                    col.render
                      ? col.render(row, column as any, $index)
                      : row[col.key],
                }}
              </ElTableColumn>
            );
          })}
        </ElTable>

        {/* 分页器 */}
        {props.showPagination && props.total > 0 && (
          <div style="margin: 10px 0 20px; padding: 12px">
            <ElPagination
              layout={props.pageLayout}
              total={props.total}
              pageSize={props.pageSize}
              pageSizes={props.pageSizeOptions}
              currentPage={props.currentPage}
              onCurrentChange={(page: number) => onPageChange(page, props.pageSize)}
              onSizeChange={(size: number) => onPageChange(1, size)}
            />
          </div>
        )}
      </Fragment>
    );
  },
});

解释一下上面代码:

写法上 ,首先是写的 tsx 文件,语法是要符合 jsx 的,平时使用的 el-table 在 jsx 中要使用大驼峰 ElTable,在 vue 中使用的自定义指令,比如 v-if 这些在 jsx 中是不生效的,因为这里不是 template 模板,不会有框架底层的编译转换。

实现过程中遇到的问题

  1. 在 .vue 文件的 script 中写诸如 render: () => <div></div> 编辑器语法会报错。

修改模板语言

xml 复制代码
<script setup lang="tsx">  // 改成 tsx

修改 .eslintrc.cjs,使支持 jsx。

yaml 复制代码
parserOptions: {
    ecmaFeatures: {
      jsx: true // 新增
    }
}

新增一个 shims-tsx.d.ts 声明,否则会有 JSX element implicitly has type any because no interface JSX.IntrinsicElements exists 的报错。

typescript 复制代码
declare global {
  namespace JSX {
    type Element = VNode;
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}
  1. 在封装过程中,由于写法的关系,elTable 不再支持 v-loading 的指令,所以需要使用ElLoading.service 的方式调用。
  2. 由于分页器一般没啥特殊逻辑,所以也将代码写在了一起,并没有抽离出去。
  3. 使用 ElPagination 分页组件时,文档是支持 onChange 的,但配置后没生效,所以实现时分别监听了 pagesize。暂时不确定问题原因,在新增透传属性或事件时需要留个心,看是否生效。

使用

vue 复制代码
<template>
  <BaseTable
    :columns="columns"
    :data="tableData"
    :loading="loading"
    :total="total"
    v-model:current-page="currentPage"
    v-model:page-size="pageSize"
    @page-change="handlePageChange"
  />
</template>

<script setup lang="tsx">
import { ref } from 'vue';

const columns = [
  {
    key: 'name',
    label: '姓名',
    sortable: true,
  },
  {
    key: 'age',
    label: '年龄',
    sortable: true,
  },
  {
    key: 'email',
    label: '邮箱',
    render: (row) => (
      <span style="color: #409eff">{row.email}</span>
    ),
  },
  {
    key: 'action',
    label: '操作',
    render: (row) => (
      <el-button size="small" onClick={() => handleEdit(row)}>
        编辑
      </el-button>
    ),
  },
];

const tableData = ref([]);
const loading = ref(false);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);

const handlePageChange = (page: number, size: number) => {
  // 获取数据逻辑
  // getTableData();
};
</script>

至此,简易版的 BaseTabe 组件就可以支持根据 columns 的配置来自定义列展示了,表头和列内容都支持函数形式,也可以根据实际使用情况,透传一些 props 或事件。

相关推荐
柯南95271 分钟前
Vue 3 Ref 源码解析
vue.js
小高00723 分钟前
面试官:npm run build 到底干了什么?从 package.json 到 dist 的 7 步拆解
前端·javascript·vue.js
JayceM1 小时前
Vue中v-show与v-if的区别
前端·javascript·vue.js
HWL56791 小时前
“preinstall“: “npx only-allow pnpm“
运维·服务器·前端·javascript·vue.js
秃头小傻蛋2 小时前
Vue 项目中条件加载组件导致 CSS 样式丢失问题解决方案
前端·vue.js
复苏季风2 小时前
vite里把markdown文件渲染成vue组件
vue.js·markdown
柯南95274 小时前
Vue 3 响应式系统源码解析
vue.js
文艺理科生4 小时前
Nuxt.js入门指南-Vue生态下的高效渲染技术
前端·vue.js·nuxt.js
夏小花花4 小时前
vue3 ref和reactive的区别和使用场景
前端·javascript·vue.js·typescript