起初我是使用 antd 的表格但是,产品要求太多,又因为 antd 的组件库长时间没更新,担心不维护了,后来(已迁移)看到作者说在准备新版本但是改动很多所以长时间没有更新,但是这时候我已经迁移完成,然后根据业务需求进行了二次封装,封装主要包含了自定义序号、自定义空数据展示、自定义分页、自定义脱敏、自定义时间格式化、处理全选、列、表头等问题
页面代码:
html
<template>
<div class="table-list">
<el-table
ref="tableRef"
v-loading="loading"
:border="border"
:data="tableData"
:row-key="getRowKeys"
:show-summary="showSummary"
@selection-change="selectionChange"
:highlight-current-row="highlightCurrentRow"
@current-change="handleCurrentChange"
@row-click="handleRowClick"
@summary-method="getSummaries"
:default-expand-all="defaultExpandAll"
size="large"
:header-row-style="headerRowStyle"
:row-style="rowStyle"
:row-class-name="`${rowClassName} custom-table-row`"
>
<template #empty>
<div class="table-empty">
<el-empty />
</div>
</template>
<el-table-column
v-if="isSelect"
type="selection"
align="left"
:reserve-selection="true"
label="全選"
width="55"
></el-table-column>
<el-table-column
v-if="isSequence"
type="index"
:label="sequenceLabel"
:width="sequenceWidth"
align="left"
:index="getSequence"
>
<!-- 可以支持自定义序号 -->
<!-- <template #default="scope">
<slot
name="sequence"
:row="scope.row"
:column="scope.column"
:$index="scope.$index"
:sequence="getSequence(scope.$index)"
>
<div class="table-sequence-cell">
{{ getSequence(scope.$index) }}
</div>
</slot>
</template> -->
</el-table-column>
<el-table-column
v-if="tableExpandProps && tableExpandProps.length > 0"
type="expand"
>
<template #default="props">
<el-form inline size="small" label-suffix=":">
<el-form-item
v-for="(item, idx) in tableExpandProps"
:key="item.id"
:label="item.label"
>
<span>{{ props.row[item.prop] }}</span>
<el-divider
v-if="idx < tableExpandProps.length - 1"
direction="vertical"
></el-divider>
</el-form-item>
</el-form>
</template>
</el-table-column>
<el-table-column
v-for="item in tableProps"
:key="item.id"
:label="item.label"
:width="item.width"
:align="item.align || 'left'"
:prop="item.prop"
:sortable="item.sortable ? true : false"
:show-overflow-tooltip="item.overflow ? false : true"
:label-class-name="item.className"
:class-name="item.cellClassName"
:fixed="item.fixed"
>
<template #header>
<div class="custom-table-header">
<el-divider class="custom-table-divider" direction="vertical" />
<span>{{ item.label }}</span>
</div>
</template>
<template #default="scope">
<slot v-if="item.slot" :row="scope.row" :name="item.slot"></slot>
<div
v-else
style="overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
>
{{ formatText(scope.row, item) }}
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="isPage"
:total="total"
@size-change="sizeChange"
@current-change="currentChange"
:current-page="pageNation.pageNum"
:page-size="pageNation.pageSize"
layout="total, prev, pager, next, sizes"
></el-pagination>
</div>
</template>
js 相关代码
TypeScript
<script setup lang="ts">
import dayjs from "dayjs";
import { ref, computed, useTemplateRef } from "vue";
import type { TableInstance } from "element-plus";
import { maskSensitiveText } from "./utils.config";
import type { MaskMode, Props, TableColumnItem } from "./types";
const props = withDefaults(defineProps<Props>(), {
loading: false,
tableData: () => [],
tableProps: () => [],
tableExpandProps: () => [],
isSelect: false,
isSequence: false,
sequenceLabel: "序号",
sequenceWidth: 60,
total: 0,
rowKey: "id",
pageNation: () => ({
pageNum: 1,
pageSize: 10,
}),
isPage: true,
showSummary: false,
highlightCurrentRow: true,
defaultExpandAll: false,
border: false,
});
const emit = defineEmits();
const tableRef = useTemplateRef<TableInstance>("tableRef");
// 格式化函数
const formatFn = computed(() => {
return {
time: (time: string | number | Date) => dayjs(time).format("YYYY-MM-DD HH:mm:ss"),
};
});
const defaultHeaderStyle = {
background: "#f8fafc",
color: "#07123C",
padding: "9px",
};
const headerRowStyle = computed(() => {
return props.headerRowStyle ? props.headerRowStyle : defaultHeaderStyle;
});
// 获取行key
function getRowKeys(row: any) {
return row[props.rowKey] || row.id;
}
// 计算序号
function getSequence(index: number) {
if (props.isPage) {
return (props.pageNation.pageNum - 1) * props.pageNation.pageSize + index + 1;
}
return index + 1;
}
// 更改当前页数触发
function currentChange(page: number) {
emit("current-change", page);
}
// 更改每页显示条数触发
function sizeChange(size: number) {
emit("size-change", size);
}
// 表格总结
function getSummaries(params: any) {
emit("get-summaries", params);
}
// 格式化表格文本
function formatText(row: any = {}, item: TableColumnItem) {
let text = row[item.prop || ""];
if (item.format && formatFn.value[item.format as keyof typeof formatFn.value]) {
const formatFunction = formatFn.value[item.format as keyof typeof formatFn.value];
text = formatFunction(row[item.prop || ""]);
}
if (item.maskSensitive) {
const mode: MaskMode =
item.maskSensitive === true ? "last" : (item.maskSensitive as MaskMode);
const preserveSpaces = item.maskSensitivePreserveSpaces ?? false;
return maskSensitiveText(text, mode, preserveSpaces);
}
return text;
}
function selectionChange(selection: any[]) {
emit("selection-change", selection);
}
// 单选选中
function handleCurrentChange(row: any) {
emit("handle-current-change", row);
}
// 行点击
function handleRowClick(row: any, column: any, event: Event) {
emit("row-click", row, column, event);
}
// 清空选择
function clearSelection() {
tableRef.value?.clearSelection();
}
// 设置当前行
function setCurrent(row: any) {
tableRef.value?.setCurrentRow(row);
}
// 切换行选中状态
function toggleRowSelection(row: any, param: boolean) {
tableRef.value?.toggleRowSelection(row, param);
}
// 暴露方法供父组件调用
defineExpose({
clearSelection,
setCurrent,
toggleRowSelection,
tableRef,
});
</script>
types.d.ts 文件
TypeScript
export type MaskMode = "first" | "last" | "middle";
export interface TableColumnItem {
id: string | number;
label: string;
prop?: string;
width?: string | number;
align?: "left" | "center" | "right";
sortable?: boolean;
overflow?: boolean;
className?: string;
cellClassName?: string;
fixed?: boolean | "left" | "right";
slot?: string;
format?: "time" | string;
maskSensitive?: boolean | MaskMode;
maskSensitivePreserveSpaces?: boolean;
}
export interface TableExpandItem {
id: string | number;
label: string;
prop: string;
}
export interface PageNation {
pageNum: number;
pageSize: number;
}
export interface Props {
loading?: boolean;
tableData?: any[];
tableProps?: TableColumnItem[];
tableExpandProps?: TableExpandItem[];
isSelect?: boolean;
isSequence?: boolean;
sequenceLabel?: string;
sequenceWidth?: string | number;
total?: number;
rowKey?: string;
pageNation?: PageNation;
isPage?: boolean;
showSummary?: boolean;
highlightCurrentRow?: boolean;
defaultExpandAll?: boolean;
border?: boolean;
headerRowStyle?: any;
rowStyle?: any;
rowClassName?: (row: any) => string;
}
utils.config.ts 文件
TypeScript
import type { MaskMode } from "./types";
const SPACE_AND_PUNCT_PATTERN = /[·\s\u2000-\u206F\u2E00-\u2E7F'"!#$%&()*+,./:;<=>?@[\\\]^_`{|}~-]/g;
/**
* 对连续非空字符串进行字符级脱敏
*/
function maskChars(str: string, mode: MaskMode): string {
const len = str.length;
if (len === 1) return "*";
if (mode === "first") {
return str[0] + "*".repeat(len - 1);
}
if (mode === "last") {
return "*".repeat(len - 1) + str[len - 1];
}
if (len === 2) {
return str[0] + "*";
}
return str[0] + "*".repeat(len - 2) + str[len - 1];
}
/**
* 对敏感文本进行脱敏(掩码)处理
* @param rawValue 原始字符串
* @param mode 脱敏模式:first | last | middle(默认 middle)
* @param preserveSpaces 是否保留原始空格位置
*/
export function maskSensitiveText(
rawValue = "",
mode: MaskMode = "middle",
preserveSpaces = false,
): string {
if (!rawValue || rawValue.trim() === "") {
return "*";
}
if (preserveSpaces) {
const chars = Array.from(rawValue);
const nonSpaces = chars.filter((char) => !/\s/.test(char));
if (nonSpaces.length === 0) return "*";
const masked = maskChars(nonSpaces.join(""), mode).split("");
let idx = 0;
return chars
.map((char) => (/\s/.test(char) ? char : masked[idx++] ?? "*"))
.join("");
}
const cleanValue = rawValue.replace(SPACE_AND_PUNCT_PATTERN, "");
if (cleanValue === "") return "*";
return maskChars(cleanValue, mode);
}
css 相关代码:
css
<style lang="scss" scoped>
.el-pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.custom-table-header {
display: flex;
align-self: center;
.custom-table-divider {
height: 22px;
background: rgba(0, 0, 0, 0.05);
margin-left: -10px;
margin-right: 10px;
}
}
:deep(tr > th:first-child > .cell > .custom-table-header > .custom-table-divider) {
display: none;
margin-left: 0;
}
:deep(.el-table .custom-table-row) {
color: #374151 !important;
}
.table-empty {
margin: 50px auto;
}
</style>
最终效果
