Vue + element plus 二次封装表格

起初我是使用 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>

最终效果

相关推荐
JarvanMo2 小时前
Flakeproof - 自动化 Flutter 的用户体验 (UX) 测试
前端
北慕阳2 小时前
速成Vue,自己看
前端·javascript·vue.js
shanyanwt2 小时前
1分钟解决iOS App Store上架图片尺寸问题
前端·ios
摇滚侠2 小时前
HTML5,CSS3,开启浮动布局后,主轴和侧轴的概念
前端·css3·html5
少云清2 小时前
【软件测试】4_基础知识 _HTML
前端·html
Want5952 小时前
HTML跳动的爱心①
前端·html
小兔崽子去哪了2 小时前
mitt 跨多层组件甚至兄弟组件通信
前端
aiguangyuan2 小时前
React中Context 的作用及原理
javascript·react·前端开发
小禾青青2 小时前
我用uniapp开发app用到的uniapp插件
前端·vue.js·uni-app