Vue移动端项目二次封装原生table组件,支持表头/数据动态配置(支持多级表头、排序);作用域插槽、render函数渲染某列数据等功能

2025-07-28 TTable组件新增多级表头及排序和斑马线功能

效果

一、最终效果

二、参数配置

1、代码示例:

html 复制代码
<t-table :columns="columns" :data="tableData" max-height="calc(100vh - 170px)" @sort-change="onSort"/>

2、配置参数(t-table Attributes)

参数 说明 类型 默认值
columns 表头配置项 Array []
data 数据源 Array []
max-height 固定表头 String -
height 表格高度 String 100%
emptyText 无数据文案 String '暂无数据'
stripe 条纹行 Boolean false

2-1、columns配置项(支持多级表头)

参数 说明 类型 默认值
label 表头名称 String -
prop 数据字段 String -
width 列宽 String -
align 对齐方式 String left
render render函数 function -
slotName 作用域插槽 String -
sortable 是否可排序 Boolean false
children 子表头配置 Array -

多级表头示例:

js 复制代码
columns: [
  { label: '姓名', prop: 'name', sortable: true },
  {
    label: '联系方式',
    children: [
      { label: '电话', prop: 'phone' },
      { label: '邮箱', prop: 'email', sortable: true }
    ]
  }
]

3、events 事件

事件名称 说明 回调参数
rowClick 行点击事件 当前行数据row,当前所在行rowIndex
sort-change 排序事件 { prop, order }

排序事件说明:

  • sort-change 事件在点击升序/降序按钮时触发,参数为 { prop, order },可用于服务端或前端排序。

三、源码

html 复制代码
<template>
  <div class="t-table-wrapper">
    <div class="t-table-scroll" :style="{ height: height, maxHeight: maxHeight }">
      <table class="t-table">
        <colgroup>
          <col
            v-for="(col, idx) in leafColumns"
            :key="idx"
            :style="{
              width: col.width
                ? typeof col.width === 'number'
                  ? col.width + 'px'
                  : col.width
                : 'auto',
            }"
          />
        </colgroup>
        <thead>
          <template v-for="(row, rowIdx) in headerRows">
            <tr :key="rowIdx">
              <th
                v-for="(cell, cellIdx) in row"
                :key="cellIdx"
                :colspan="cell.colspan"
                :rowspan="cell.rowspan"
                :style="{
                  textAlign: cell.align ? cell.align : 'left',
                  fontWeight:cell.bold ? cell.bold : 'bold',
                  position: 'sticky',
                  top: `${rowIdx * 11.2}vw`,
                  zIndex: 2,
                }"
              >
                <span>{{ cell.label }}</span>
                <span class="sort_table" v-if="cell.sortable && cell.prop">
                  <span
                    class="sort-btn"
                    :class="{ active: sortProp === cell.prop && sortOrder === 'asc' }"
                    @click.stop="changeSort(cell.prop, 'asc')"
                    title="升序"
                  >▲</span>
                  <span
                    class="sort-btn"
                    :class="{ active: sortProp === cell.prop && sortOrder === 'desc' }"
                    @click.stop="changeSort(cell.prop, 'desc')"
                    title="降序"
                  >▼</span>
                </span>
              </th>
            </tr>
          </template>
        </thead>
        <tbody>
          <template v-if="data && data.length">
            <tr
              v-for="(row, rowIndex) in data"
              :key="rowIndex"
              @click="rowClick(row, rowIndex)"
              :class="{ 't-table-row-striped': rowIndex % 2 === 1 && stripe }"
            >
              <td
                v-for="(col, colIndex) in leafColumns"
                :key="colIndex"
                :style="{
                  textAlign: col.align ? col.align : 'left',
                  width: col.width
                    ? typeof col.width === 'number'
                      ? col.width + 'px'
                      : col.width
                    : 'auto',
                }"
              >
                <template v-if="col.slotName">
                  <slot :name="col.slotName" :scope="row"></slot>
                </template>
                <template v-if="col.render">{{col.render(row, rowIndex)}}</template>
                <template v-if="!col.render && !col.slotName">
                  {{
                  row[col.prop]
                  }}
                </template>
              </td>
            </tr>
          </template>
          <template v-else>
            <tr>
              <td
                :colspan="leafColumns.length"
                style="text-align: center; color: #999; padding: 20px 0"
              >{{ emptyText }}</td>
            </tr>
          </template>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script>
export default {
  name: "TTable",
  data() {
    return {
      sortProp: "",
      sortOrder: "",
    };
  },
  props: {
    columns: {
      type: Array,
      required: true,
    },
    data: {
      type: Array,
      required: true,
    },
    height: {
      type: String,
      default: "100%",
    },
    maxHeight: {
      type: String,
      default: "",
    },
    emptyText: {
      type: String,
      default: "暂无数据",
    },
    stripe: {
      type: Boolean,
      default: false,
    },
  },
  computed: {
    headerRows() {
      return this.getHeaderRows(this.columns);
    },
    leafColumns() {
      return this.getLeafColumns(this.columns);
    },
  },
  methods: {
    changeSort(prop, order) {
      if (this.sortProp === prop && this.sortOrder === order) {
        this.sortProp = '';
        this.sortOrder = '';
        this.$emit('sort-change', { prop: '', order: '' });
      } else {
        this.sortProp = prop;
        this.sortOrder = order;
        this.$emit('sort-change', { prop, order });
      }
    },
    rowClick(row, rowIndex) {
      this.$emit("row-click", row, rowIndex);
    },
    getHeaderRows(columns, level = 0, rows = []) {
      rows[level] = rows[level] || [];
      columns.forEach(col => {
        const cell = {
          label: col.label,
          align: col.align,
          colspan: 1,
          rowspan: 1,
          prop: col.prop,
          sortable: col.sortable,
        };
        if (col.children && col.children.length) {
          cell.colspan = this.getLeafColumns(col.children).length;
          cell.rowspan = 1;
          rows[level].push(cell);
          this.getHeaderRows(col.children, level + 1, rows);
        } else {
          cell.rowspan = this.getMaxLevel(columns) - level;
          rows[level].push(cell);
        }
      });
      return rows;
    },

    getLeafColumns(columns) {
      let result = [];
      columns.forEach(col => {
        if (col.children && col.children.length) {
          result = result.concat(this.getLeafColumns(col.children));
        } else {
          result.push(col);
        }
      });
      return result;
    },

    getMaxLevel(columns, level = 1) {
      let max = level;
      columns.forEach(col => {
        if (col.children && col.children.length) {
          const childLevel = this.getMaxLevel(col.children, level + 1);
          if (childLevel > max) max = childLevel;
        }
      });
      return max;
    }
  },
};
</script>

<style lang="scss" scoped>
.t-table-wrapper {
  width: 100%;
  overflow-x: auto;
  background-color: #fff;
  .t-table-scroll {
    min-width: 100%;
    overflow: auto;
    max-height: 100%;
    .t-table {
      width: max-content;
      border-collapse: collapse;
      min-width: 100%;
      th,
      td {
        border: 1px solid #c8c8c8;
        height: 42px;
        padding: 0 10px;
        text-align: left;
        white-space: nowrap;
        font-size: 14px;
        white-space: normal; // 支持换行
        word-break: break-all; // 长单词或长内容换行
      }
      th {
        background: #f5f5f5;
        position: relative;
      }
      .sort_table {
        display: flex;
        align-items: center;
        flex-direction: column;
        position: absolute;
        right: 4px;
        top: 50%;
        transform: translateY(-50%);
        .sort-btn {
          cursor: pointer;
          margin-left: 2px;
          font-size: 12px;
          color: #bbb;
          &.active {
            color: #1890ff;
            font-weight: bold;
          }
        }
      }
      .t-table-row-striped {
        background-color: #f5f5f5; // 条纹行背景色
      }
    }
  }
}
</style>

相关文章

基于ElementUi再次封装基础组件文档


vue3+ts基于Element-plus再次封装基础组件文档

相关推荐
OpenTiny社区1 小时前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
ReturnTrue8682 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
武昌库里写JAVA3 小时前
「mysql」Mac osx彻底删除mysql
vue.js·spring boot·毕业设计·layui·课程设计
Rika3 小时前
手写mini-vue之后,我写了一份面试通关手册
前端·vue.js
咔咔一顿操作4 小时前
常见问题三
前端·javascript·vue.js·前端框架
NicolasCage7 小时前
Icon图标库推荐
vue.js·react.js·icon
一道雷7 小时前
🧩 Vue Router嵌套路由新范式:无需嵌套 RouterView 的布局实践
前端·vue.js
Ares-Wang7 小时前
Vue》》@ 用法
前端·javascript·vue.js
张志鹏PHP全栈9 小时前
Vue3第五天,ref 和 reactive的介绍和区别
前端·vue.js