手摸手带你封装Vue组件库(11)Table表格组件(上)

1,401 阅读6分钟

本节开始我们很常用的 Table 组件的开发,主要实现以下功能:

基础使用、自定义列、斑马线、固定表头、固定列、多选。

创建组件和样式文件

我们创建如图所示的组件目录结构。

project-20250120-1.png

这次有人会好奇为什么我没有给每一列去创建一个 table-column 的组件,而是直接在 table 组件中实现,因为在列多的情况下我们的 html 会显得特别繁琐,当然我们也会实现自定义列的相关功能,后面会提到如何实现。

基础使用

我们需要设置我们表格中存在的列都有哪些数据,对应的 key 是什么,以及我每列对应的宽度是多少,排列方式是什么,这样我们就得出了我们的 column 属性应该大体是什么样子了。如果用 TS 来描述就是: ··

interface ColumnItem {
  key: string;
  title: string;
  width?: number;
  align?: "left" | "center" | "right";
}

然后我们需要传递表格中的数据,每行也就是对应每条数据,其中每一个 key 对应一个列也对应一个值,这样我们的 data 就和我们的 column 对应起来了。我们定义一下 table 组件的属性:

table.js

export const TableProps = {
  columnData: {
    type: Array,
    default: () => [],
  },
  tableData: {
    type: Array,
    default: () => [],
  },
};

我们在我们示例项目中使用一下这个组件:

<template>
  <div>
    <t-table :column-data="columnData" :table-data="tableData"> </t-table>
  </div>
</template>

<script setup lang="ts">
const columnData = [
  {
    key: 'date',
    label: 'Date',
    width: "120",
  },
  {
    key: 'name',
    label: 'Name'
  },
  {
    key: 'address',
    label: 'Address'
  },
]

const tableData = [
  {
    date: '2016-05-03',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-04',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-01',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
]

这时候我们来写一下 table 组件基本的结构

<template>
  <div class="t-table">
    <div class="t-table__thead-wrapper">
      <table class="t-table__thead">
        <thead>
          <tr>
            <th v-for="theadItem in columnData" :key="theadItem.key">
              <div class="cell">{{ theadItem.label }}</div>
            </th>
          </tr>
        </thead>
      </table>
    </div>
    <div class="t-table__tbody-wrapper">
      <table class="t-table__tbody">
        <tbody>
          <tr v-for="(rowItem, index) in tableData" :key="'table_row_' + index">
            <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
              <div class="cell">{{ rowItem[colItem.key] }}</div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
  import { TableProps } from "./table";

  defineOptions({
    name: "t-table",
  });

  const props = defineProps(TableProps);
</script>

这时候就这个样子

project-20250120-2.png

我们来设置一下样式

.t-table {
  color: var(--t-text-color);
  .t-table__thead-wrapper {
    border-bottom: 1px solid var(--t-border-color);
  }
  .t-table__thead,
  .t-table__tbody {
    width: 100%;
  }
  .t-table__thead tr,
  .t-table__tbody tr {
    display: flex;
    align-items: center;
  }
  .t-table__thead tr > th,
  .t-table__tbody tr > td {
    padding: 8px 0;
    flex: 1;
    text-align: left;
  }
  .t-table__tbody tr {
    border-bottom: 1px solid var(--t-border-color);
    &:hover {
      background-color: var(--t-hover-color);
    }
  }
  .cell {
    padding: 0 10px;
  }
}

这下看一下,大概的 table 样子就出来了

project-20250120-3.png

我们默认是每列都是 flex: 1,也就是均分,但是我们给每列可以设置宽度,所以这时候在列这边修改一下

<template>
  <div class="t-table">
    <div class="t-table__thead-wrapper">
      <table class="t-table__thead">
        <thead>
          <tr>
            <th
              v-for="theadItem in columnData"
              :key="theadItem.key"
              :style="{
                width: theadItem.width + 'px',
                flex: theadItem.width ? 'none' : 1,
              }"
            >
              <div class="cell">{{ theadItem.label }}</div>
            </th>
          </tr>
        </thead>
      </table>
    </div>
    <div class="t-table__tbody-wrapper">
      <table class="t-table__tbody">
        <tbody>
          <tr v-for="(rowItem, index) in tableData" :key="'table_row_' + index">
            <td
              v-for="(colItem, i) in columnData"
              :key="'table_col_' + i"
              :style="{
                width: colItem.width + 'px',
                flex: colItem.width ? 'none' : 1,
              }"
            >
              <div class="cell">{{ rowItem[colItem.key] }}</div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

project-20250120-4.png

这时候觉得使用 flex 已经丸美解决了,但是后续会存在一个问题,这边我最后还是选择使用 colgroup 来解决

<colgroup> 标签用于规定表格中包含一列或多列的分组的格式。

因为我们表头和表格内容是分开的,所以我们需要在表头和表格内容都添加一个 colgroup 标签,然后设置统一的宽度,因为如果不设置宽度,上下两个 colgroup 标签的宽度会不一致,导致表格错乱

<template>
  <div class="t-table" ref="tableRef">
    <div class="t-table__thead-wrapper">
      <table class="t-table__thead">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width"
          />
        </colgroup>
        <thead>
          <tr>
            <th v-for="theadItem in columnData" :key="theadItem.key">
              <div class="cell">{{ theadItem.label }}</div>
            </th>
          </tr>
        </thead>
      </table>
    </div>
    <div class="t-table__tbody-wrapper">
      <table class="t-table__tbody">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width"
          />
        </colgroup>
        <tbody>
          <tr v-for="(rowItem, index) in tableData" :key="'table_row_' + index">
            <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
              <div class="cell">{{ rowItem[colItem.key] }}</div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

这时候如果不设置列宽,表头和表格的内容列宽自动的时候宽度将不一样,所以我们需要计算一下没有设置列宽的列的宽度,然后均分

<template>
  <div class="t-table" ref="tableRef">
    <div class="t-table__thead-wrapper">
      <table class="t-table__thead">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width || averageWidth"
          />
        </colgroup>
        <thead>
          <tr>
            <th v-for="theadItem in columnData" :key="theadItem.key">
              <div class="cell">{{ theadItem.label }}</div>
            </th>
          </tr>
        </thead>
      </table>
    </div>
    <div class="t-table__tbody-wrapper">
      <table class="t-table__tbody">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width || averageWidth"
          />
        </colgroup>
        <tbody>
          <tr v-for="(rowItem, index) in tableData" :key="'table_row_' + index">
            <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
              <div class="cell">{{ rowItem[colItem.key] }}</div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
  import { nextTick, onMounted, ref, watch } from "vue";
  import { TableProps } from "./table";

  defineOptions({
    name: "t-table",
  });

  const props = defineProps(TableProps);
  const tableRef = ref(null);
  const averageWidth = ref(0);

  const calcColumnWidth = () => {
    const tableWrapperWidth = tbodyWrapperRef.value.offsetWidth;
    const columnWidthArrs = props.columnData
      .filter((item) => item.width)
      .map((item) => Number(item.width));
    const sumOfColumnWidth = columnWidthArrs.reduce((x, y) => x + y, 0);
    averageWidth.value =
      (tableWrapperWidth - sumOfColumnWidth) /
      (props.columnData.length - columnWidthArrs.length);
  };

  watch(
    props.columnData,
    () => {
      nextTick(() => {
        calcColumnWidth();
      });
    },
    {
      immediate: true,
    }
  );
</script>
.t-table {
  color: var(--t-text-color);
  .t-table__thead-wrapper {
    border-bottom: 1px solid var(--t-border-color);
  }
  .t-table__thead,
  .t-table__tbody {
    width: 100%;
    border-spacing: 0;
    border-collapse: collapse;
  }
  .t-table__thead tr > th,
  .t-table__tbody tr > td {
    padding: 8px 0;
    text-align: left;
  }
  .t-table__tbody tr {
    border-bottom: 1px solid var(--t-border-color);
    &:hover {
      background-color: var(--t-hover-color);
    }
  }
  .cell {
    padding: 0 10px;
  }
}

这样就可以了,但是有时候我们屏幕大小在变动,所以我们需要监听一下屏幕大小变化,重新计算一下

onMounted(() => {
  window.addEventListener("resize", calcColumnWidth);
});

onUnmounted(() => {
  window.removeEventListener("resize", calcColumnWidth);
});

斑马纹

这个简单,我们定义一个属性 stripe,接受一个 boolean 值,默认为 false

export const TableProps = {
  columnData: {
    type: Array,
    default: () => [],
  },
  tableData: {
    type: Array,
    default: () => [],
  },
  stripe: {
    type: Boolean,
    default: false,
  },
};

然后添加一下类名即可

<!-- ... -->
<table class="t-table__tbody">
  <colgroup>
    <col
      v-for="colItem in columnData"
      :key="'table_col_' + colItem.key"
      :width="colItem.width || averageWidth"
    />
  </colgroup>
  <tbody>
    <tr
      v-for="(rowItem, index) in tableData"
      :key="'table_row_' + index"
      :class="{
              't-table__tbody--stripe': stripe && index % 2 === 1,
            }"
    >
      <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
        <div class="cell">{{ rowItem[colItem.key] }}</div>
      </td>
    </tr>
  </tbody>
</table>
<!-- ... -->
.t-table {
  // ...
  .t-table__tbody--stripe {
    background-color: #f8f8f8;
  }
}

边框

我们定义一个属性 border,接受一个 boolean 值,默认为 false

export const TableProps = {
  columnData: {
    type: Array,
    default: () => [],
  },
  tableData: {
    type: Array,
    default: () => [],
  },
  stripe: {
    type: Boolean,
    default: false,
  },
  border: {
    type: Boolean,
    default: false,
  },
};

我们在最外部添加一个类名,然后在这个类下添加边框即可

<div :class="['t-table', border ? 't-table--border' : '']">
  <!-- ... -->
</div>

添加一下样式

.t-table--border {
  border: 1px solid var(--t-border-color);
  .t-table__tbody tr:nth-last-child(1) {
    border: none;
  }
  .t-table__thead tr > th,
  .t-table__tbody tr > td {
    box-sizing: border-box;
    border-right: 1px solid var(--t-border-color);
  }
  .t-table__thead tr > th:nth-last-child(1),
  .t-table__tbody tr > td:nth-last-child(1) {
    border-right: none;
  }
}

project-20250120-5.png

自定义列

我们经常在开发的过程中需要修改某列的显示内容,element-plus 是在各自的 el-table-column 组件的默认插槽中定义的,那我们改如何设置呢?

其实也很简单,我们可以设置具名插槽,具名插槽的名称就为我们每列的 key

<!-- ... -->
<div class="t-table__tbody-wrapper">
  <table class="t-table__tbody">
    <colgroup>
      <col
        v-for="colItem in columnData"
        :key="'table_col_' + colItem.key"
        :width="colItem.width || averageWidth"
      />
    </colgroup>
    <tbody>
      <tr
        v-for="(rowItem, index) in tableData"
        :key="'table_row_' + index"
        :class="{
              't-table__tbody--stripe': stripe && index % 2 === 1,
            }"
      >
        <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
          <div class="cell">
            <slot :name="colItem.key"> {{ rowItem[colItem.key] }} </slot>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>
<!-- ... -->

比如我们需要修改 address 这列

<t-table :column-data="columnData" :table-data="tableData">
  <template #address> 我要自己定义 </template>
</t-table>

project-20250120-6.png

这样就可以自定义了

但是,我们一般都需要在这块根据当前的值来做进一步的转换,所以我们需要在插槽中获取到当前行的数据,我们最常用的就是当前行的数据以及当前行的下标。

<!-- ... -->
<div class="t-table__tbody-wrapper">
  <table class="t-table__tbody">
    <colgroup>
      <col
        v-for="colItem in columnData"
        :key="'table_col_' + colItem.key"
        :width="colItem.width || averageWidth"
      />
    </colgroup>
    <tbody>
      <tr
        v-for="(rowItem, index) in tableData"
        :key="'table_row_' + index"
        :class="{
              't-table__tbody--stripe': stripe && index % 2 === 1,
            }"
      >
        <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
          <div class="cell">
            <slot
              :name="colItem.key"
              v-bind="{ scoped: rowItem, $index: index }"
            >
              {{ rowItem[colItem.key] }}
            </slot>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>
<!-- ... -->

我们可以这样获取

<t-table :column-data="columnData" :table-data="tableData">
  <template #address="{ scoped, $index }">
    这是第{{ $index }}行数据,内容是{{ scoped }}
  </template>
</t-table>

project-20250120-7.png

固定表头

因为默认我们是内容有多少,表格的 body 部分就有多长,固定表头的时候就需要我们设定高度,超过这个高度表格的 body 部分才会滚动,所以我们需要定义一个属性 height

export const TableProps = {
  columnData: {
    type: Array,
    default: () => [],
  },
  tableData: {
    type: Array,
    default: () => [],
  },
  stripe: {
    type: Boolean,
    default: false,
  },
  border: {
    type: Boolean,
    default: false,
  },
  height: {
    type: Number,
  },
};
<!-- ... -->
<div
  class="t-table__tbody-wrapper"
  :style="{
        height: height ? height + 'px' : 'auto',
        overflow: height ? 'auto' : 'hidden',
      }"
>
  <table class="t-table__tbody">
    <!-- ... -->
  </table>
</div>
<!-- ... -->

我们来看看

<t-table :column-data="columnData" :table-data="tableData" :height="260">
</t-table>

project-20250120-8.png

感觉确实可以,但是其实是有问题的,我们这时候给固定表头的这个设置上 border 再来看一下

project-20250120-9.png

这时候就会发现,表格的 body 部分和表头部分没有对齐,这是因为边框占据了 body 的宽度,导致内部的空间缩减,这时候有三种方式来解决这个问题:

  1. 直接给表格的 body 部分的滚动条设置隐藏,这样就不会占据宽度了,但是我们看过去不知道当前的内容是不是还有,可以滚动,所以不推荐
  2. 判断表格的 body 的高度是否比 body 中内容的总体高度还大,如果大的时候我们给 thead 部分的 colgroup 中设置一个 col,这个 col 必须和滚动条宽度一致,这样的话等于上下存在的空间都是相同的,这样也就不会出现设置边框的时候对不齐的情况了
  3. 隐藏现有滚动条,手写一个滚动条,该滚动条为绝对定位,在表格的右侧,这样就可以保证滚动条不会占据表格的宽度,当然这种很麻烦,需要你计算位置、添加鼠标相关的事件等等,element-plus 就是采用该方案。

project-20250120-10.png

我还是带大家想一下 element-plus 的 table 滚动条实现思路。

思路如下:

  1. 先把滚动条隐藏
  2. 然后创建一个 div 当做滚动条的轨道,然后在里面再创建一个 div 作为滚动条的滑块,滚动条设置绝对定位
  3. 获取 tbody 视口的高度跟内容的高度作比较,如果比 tbody 的容器高度还大的时候表明要显示滚动条,否则隐藏滚动条
  4. 手写拖动逻辑,鼠标在滑块上按下的时候记录,然后鼠标移动,上下移动了多少,滚动条的滑块移动多少,同时通过计算出内容需要滚动多少,公式为:滚动条的 scrollTop/(tbody 视口高度 - 滚动条高度) = tbody 的 scrollTop/tbody 内容高度 - tbody 视口高度
  5. 当鼠标移入到 tbody 中的时候,监听鼠标滚动事件,每滚动一次,滚动条移动多少,然后根据上述的公式再计算内容需要滚动多少
  6. 鼠标移入表格区域,如果表格需要滚动则出现滚动条,移开的时候隐藏

大家只要理解这个思路都是可以实现的,只是工作量较大,我们这边主要实现第二种方案。

border 模式下固定表头实现

先判断当前 table 是否为可滚动的,并设置类名

<template>
  <div
    :class="[
      't-table',
      border ? 't-table--border' : '',
      isOverflow ? 't-table__has_scroll' : '',
    ]"
    ref="tableRef"
  >
    <div class="t-table__thead-wrapper">
      <!-- ... -->
    </div>
    <div
      class="t-table__tbody-wrapper"
      :style="{
        height: height ? height + 'px' : 'auto',
        overflow: height ? 'auto' : 'hidden',
      }"
      ref="tbodyWrapperRef"
    >
      <table class="t-table__tbody">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width || averageWidth"
          />
        </colgroup>
        <tbody>
          <!-- ... -->
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
  import { onMounted, ref } from "vue";
  import { TableProps } from "./table";

  defineOptions({
    name: "t-table",
  });

  const props = defineProps(TableProps);
  const tbodyWrapperRef = ref(null);
  const isOverflow = ref(false);

  const calcColumnWidth = () => {
    const tableWrapperWidth = tbodyWrapperRef.value.offsetWidth;
    const tbodyWrapperHeight = tbodyWrapperRef.value.offsetHeight;
    const scrollHeight = tbodyWrapperRef.value.scrollHeight;
    isOverflow.value = scrollHeight > tbodyWrapperHeight;

    const columnWidthArrs = props.columnData
      .filter((item) => item.width)
      .map((item) => Number(item.width));
    const sumOfColumnWidth = columnWidthArrs.reduce(
      (x, y) => x + y, 0
    );

    // 这里我们-8是因为滚动条的宽度是8px我们需要减掉再平均
    averageWidth.value = isOverflow.value
      ? (tableWrapperWidth - sumOfColumnWidth - 8) /
        (props.columnData.length - columnWidthArrs.length)
      : (tableWrapperWidth - sumOfColumnWidth) /
        (props.columnData.length - columnWidthArrs.length);
  };
</script>

我们重新设置一下 tbody 部分滚动条样式

.t-table__tbody-wrapper {
  &::-webkit-scrollbar {
    width: 8px;
  }

  &::-webkit-scrollbar:horizontal {
    height: 8px;
  }
  &::-webkit-scrollbar-track {
    border-left: 1px solid var(--t-border-color);
  }

  &::-webkit-scrollbar-thumb {
    background-color: #ddd;
    border-radius: 4px;
  }
}

现在是这个样子

project-20250120-11.png

然后我们在 theadcolgroup 中在可以滚动的情况下设置一个 col,并且宽度为 8px,与滚动条的宽度保持一致

<template>
  <div
    :class="[
      't-table',
      border ? 't-table--border' : '',
      isOverflow ? 't-table__has_scroll' : '',
    ]"
    ref="tableRef"
  >
    <div class="t-table__thead-wrapper">
      <table class="t-table__thead">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width || averageWidth"
          />
          <col width="8" v-if="isOverflow" />
        </colgroup>
        <!-- ... -->
      </table>
    </div>
    <!-- ... -->
  </div>
</template>

再来看一下

project-20250120-12.png

这时候我们去掉 border 属性看一下,结果又出现了两个问题,一个是滚动条的边框,一个是 thead 的下边框线比 tbody 的宽,我们再来处理一下

.t-table {
  color: var(--t-text-color);
  .t-table__thead-wrapper {
    th {
      border-bottom: 1px solid var(--t-border-color);
    }
  }
  .t-table__thead,
  .t-table__tbody {
    width: 100%;
    border-spacing: 0;
    border-collapse: collapse;
  }
  .t-table__thead tr > th,
  .t-table__tbody tr > td {
    padding: 8px 0;
    text-align: left;
  }
  .t-table__tbody tr {
    border-bottom: 1px solid var(--t-border-color);
    &:hover {
      background-color: var(--t-hover-color);
    }
  }
  .t-table__tbody--stripe {
    background-color: #f8f8f8;
  }
  .cell {
    padding: 0 10px;
  }
}

.t-table--border {
  border: 1px solid var(--t-border-color);
  .t-table__thead-wrapper {
    th {
      border-bottom: none;
    }
  }
  .t-table__tbody tr:nth-last-child(1) {
    border: none;
  }
  .t-table__thead tr > th,
  .t-table__tbody tr > td {
    box-sizing: border-box;
    border-right: 1px solid var(--t-border-color);
  }
  .t-table__thead tr > th:nth-last-child(1),
  .t-table__tbody tr > td:nth-last-child(1) {
    border-right: none;
  }
  .t-table__tbody-wrapper::-webkit-scrollbar-track {
    border-left: 1px solid var(--t-border-color);
  }

  .t-table__tbody-wrapper {
    border-top: 1px solid var(--t-border-color);
  }
}

.t-table__tbody-wrapper {
  // 修改滚动条宽度
  &::-webkit-scrollbar {
    width: 8px;
  }

  &::-webkit-scrollbar:horizontal {
    height: 8px;
  }

  &::-webkit-scrollbar-thumb {
    background-color: #ddd;
    border-radius: 4px;
  }
}

project-20250120-13.png

丸美了

列对齐

有时候我们需要设置列的对齐方式,比如居中,靠左,靠右。我们可以在定义列的时候设置 align 属性来决定我们当前列的对齐方式,在不设置的时候我们默认为靠左。

const columnData = [
  {
    key: "date",
    label: "Date",
    width: "120",
  },
  {
    key: "name",
    label: "Name",
    align: "center",
  },
  {
    key: "address",
    label: "Address",
  },
];

我们给 cell 添加一个 text-align 样式,然后根据 align 属性来决定它的值

<template>
  <div
    :class="[
      't-table',
      border ? 't-table--border' : '',
      isOverflow ? 't-table__has_scroll' : '',
    ]"
    ref="tableRef"
  >
    <div class="t-table__thead-wrapper">
      <table class="t-table__thead">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width || averageWidth"
          />
          <col width="8" v-if="isOverflow" />
        </colgroup>
        <thead>
          <tr>
            <th v-for="theadItem in columnData" :key="theadItem.key">
              <div
                class="cell"
                :style="{
                  textAlign: theadItem.align || 'left',
                }"
              >
                {{ theadItem.label }}
              </div>
            </th>
          </tr>
        </thead>
      </table>
    </div>
    <div
      class="t-table__tbody-wrapper"
      :style="{
        height: height ? height + 'px' : 'auto',
        overflow: height ? 'auto' : 'hidden',
      }"
      ref="tbodyWrapperRef"
    >
      <table class="t-table__tbody">
        <colgroup>
          <col
            v-for="colItem in columnData"
            :key="'table_col_' + colItem.key"
            :width="colItem.width || averageWidth"
          />
        </colgroup>
        <tbody>
          <tr
            v-for="(rowItem, index) in tableData"
            :key="'table_row_' + index"
            :class="{
              't-table__tbody--stripe': stripe && index % 2 === 1,
            }"
          >
            <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
              <div
                class="cell"
                :style="{
                  textAlign: colItem.align || 'left',
                }"
              >
                <slot
                  :name="colItem.key"
                  v-bind="{ scoped: rowItem, $index: index }"
                >
                  {{ rowItem[colItem.key] }}
                </slot>
              </div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

project-20250120-14.png

这时候看是没有问题的,但其实还是不行的,你们还记得我们上面的数据每个位置是一个插槽,也就意味着他可以放任何东西,不仅仅是文字,当如果是非行级元素呢? 我们来试试

<t-table :column-data="columnData2" :table-data="tableData" stripe border>
  <template #name="{ scoped, $index }">
    <div style="width: 50px; height: 20px; background-color: pink"></div>
  </template>
</t-table>

project-20250120-15.png

这样就不行了,所以我们还得处理一下,我们只需要修改 tbody 部分就行

先来修改一下样式,让插槽改为 flex 盒子

.t-table__tbody .cell {
  display: flex;
  flex-wrap: wrap;
}

因为 justify-content 接收的值跟 text-align 值不一样,所以我们需要做映射

<template>
  <!-- .... -->
  <tbody>
    <tr
      v-for="(rowItem, index) in tableData"
      :key="'table_row_' + index"
      :class="{
              't-table__tbody--stripe': stripe && index % 2 === 1,
            }"
    >
      <td v-for="(colItem, i) in columnData" :key="'table_col_' + i">
        <div
          class="cell"
          :style="{
                  textAlign: colItem.align || 'left',
                  justifyContent: colItem.align
                    ? justifyContentMap[colItem.align]
                    : 'flex-start',
                }"
        >
          <slot :name="colItem.key" v-bind="{ scoped: rowItem, $index: index }">
            {{ rowItem[colItem.key] }}
          </slot>
        </div>
      </td>
    </tr>
  </tbody>
  <!-- ... -->
</template>
<script setup>
  const justifyContentMap = {
    left: "flex-start",
    center: "center",
    right: "flex-end",
  };
</script>

这下好了

project-20250120-16.png

由于table的功能很多,一节讲不完,我们下节继续

本专栏源码地址