背景
vue3 项目中使用的是 element-plus
的 table
组件,页面中常用的功能,大概包含 排序 ,多选 以及 列的自定义展示,所以很有封装的必要,否则每次都要写很长一串。
由于 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 呢?
实现
- 确认安装了
@vitejs/plugin-vue-jsx
插件,并在vite.config.ts
中进行了使用。 - 实现
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 模板,不会有框架底层的编译转换。
实现过程中遇到的问题:
- 在 .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;
}
}
}
- 在封装过程中,由于写法的关系,
elTable
不再支持v-loading
的指令,所以需要使用ElLoading.service
的方式调用。 - 由于分页器一般没啥特殊逻辑,所以也将代码写在了一起,并没有抽离出去。
- 使用
ElPagination
分页组件时,文档是支持onChange
的,但配置后没生效,所以实现时分别监听了page
和size
。暂时不确定问题原因,在新增透传属性或事件时需要留个心,看是否生效。
使用
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
或事件。