尝试用 Vue 写一个表格组件

LibraHeresy
• 阅读 333

前言

工作中接到一个小程序表格展示信息的需求,让我一惊,因为Taro UI Vue3这个框架根本没有表格组件。

没办法咯,只能自己写了,因为没写过表格组件,心里还是有点畏惧的。但是写完以后,我发现在这个gird网格布局大行其道的时代,编写一个实现基本功能的表格组件并不难。

特别是有很多成功的框架珠玉在前,比如Antd,所以我仿照Antd的表格组件实现了一个青春版。

给阅读这篇博客的人提个醒,我没看过Antd源码,以下实现代码是我自己写的。😂

需求

  1. 表格的结构和数据分离
  2. 默认表格宽度根据容器宽度均分,有width属性使用自定义宽度
  3. 无数据提示
  4. 控制表格边框展示
  5. 斑马纹

实现

释义

讲一下我用到的不太常见的属性。

1. display: grid

设置元素为网格布局。

2. grid-template-columns

这个属性的书面定义是基于网格列的维度,去定义网格线的名称和网格轨道的尺寸大小,用口语就是设置网格一个方向的宽度并可以对其命名。

思路

首先数据结构我仿照Antd分为两个数组,分为columns(表头结构)和dataSource(表身数据)两个对象数组。

export default {
  ......
  props: {
    columns: {
      type: Array,
      default: () => [],
    },
    dataSource: {
      type: Array,
      default: () => [],
    },
  }
}

然后将表头和表身分开渲染,表头直接循环columns渲染;表身先循环dataSource取出对象,再循环columns取值渲染。

<template>
  ......

  <view class="table-header grid" :style="colStyle">
    <view
      v-for="column in columns"
      :id="column.dataIndex"
      :key="column"
      class="header-item"
    >
      {{ column.title }}
    </view>
  </view>
  <view class="table-content">
    <view
      v-for="(data, index) in dataSource"
      :key="data"
      class="content-col grid"
      :style="colStyle"
    >
      <view
        v-for="dataColumn in columns"
        :key="index + dataColumn.dataIndex"
        class="col-item table-font"
      >
        {{ data[dataColumn.dataIndex] }}
      </view>
    </view>
  </view>
  ......
</template>

计算列宽度,通过动态添加grid-template-columns属性实现宽度自适应。

这个1fr属性就像是flex弹性布局子元素的flex: 1属性,自动填充剩下的宽度,如果有其他相同属性的元素,则按设定数值的比例分配宽度。

const colStyle = computed(() => {
  const number = props.columns.length;
  return {
    "grid-template-columns": setWidth(),
  };
});

const setWidth = () => {
  return props.columns
    .map((item) => {
      return item.width ? item.width : "1fr";
    })
    .join(" ");
};

最后添加斑马纹、边框、无数据提示,就完成了。

<template>
  <!-- showBorder 边框 -->
  <view
    class="table"
    :class="{
      'border-left': showBorder,
    }"
  >
    <view class="table-header grid" :style="colStyle">
      <view
        v-for="column in columns"
        :id="column.dataIndex"
        :key="column"
        class="header-item"
        :class="{
          'border-right': showBorder,
        }"
      >
        {{ column.title }}
      </view>
    </view>
    <view class="table-content">
      <view
        v-for="(data, index) in dataSource"
        :key="data"
        class="content-col grid"
        :style="colStyle"
      >
        <!-- zebra-color 斑马纹 -->
        <view
          v-for="dataColumn in columns"
          :key="index + dataColumn.dataIndex"
          class="col-item table-font"
          :class="{
            'zebra-color': index % 2 !== 0,
            'border-right': showBorder,
          }"
        >
          {{ data[dataColumn.dataIndex] }}
        </view>
      </view>
      <!-- 无数据提示 -->
      <view v-if="dataSource.length === 0 && showEmpty" class="tips">
        <image class="tips-img" src="./list.png" mode="aspectFit" />
        <view class="tips-text"> 暂无数据 </view>
      </view>
    </view>
  </view>
</template>
export default {
  ......
  props: {
    ......
    showEmpty: {
      type: Boolean,
      default: true,
    },
    showBorder: {
      type: Boolean,
      default: false,
    },
  }
}

最后我们来看看最终实现效果。

尝试用 Vue 写一个表格组件

尝试用 Vue 写一个表格组件

拓展

fixed 属性

产品总会提出一些很“合理”的需求,比如右边列固定,问题是我要自己写啊!

没办法了,继续薅Antd的羊毛吧,在columns对象数组里面的对象上添加fixed属性。

方案

实现右边列固定我想过两个方案:

1. 两个表格,一个表格固定在右边,用来展示有fixed属性的列
2. position:sticky,粘性定位

方案 1 简单,不用修改原有组件,但是我发现,会出现左右两个表格高度不一,所以放弃。

方案 2 ,很完美的一个 css 属性,需要注意的是生效先决条件比较多,建议看看网上的 探究 position-sticky 失效问题 这篇博客后,再开始使用粘性定位。

最后选择方案 2

问题

实现途中还是出现了一些问题。

1. 元素堆叠

我发现粘性定位会堆叠在一起,需要你手动调整元素位置,所以我选择将fixed列都放到一起,用一个容器包裹,这样只需要控制父容器位置就可以了。

那父容器的宽度如何设置呢,特别是fixed列单位有多种,如%px,很自然的,我想到了计算属性calc(),直接使用计算属性得出总宽度即可。

2. 宽度失准

我在外面设置的列宽度是根据表格宽度来设置的,但是用父容器包裹以后,锚点就从表格变成了父容器了,会出现宽度失准的问题。

但其实父容器的宽度就等于fixed列的总宽度,我们只要保证在父容器内部,各列的比例不变就可以实现宽度校准,用flex弹性布局的flex属性就可以实现这个效果。

具体代码

判断左右,控制组件展示

<!-- 表头 -->
<template>
  ......

  <view class="table-header grid" :style="colStyle">
    <template v-if="!!fixedLeftCol.length">
      <view style="left: 0px" class="fixed">
        <view
          v-for="column in fixedLeftCol"
          :id="column.dataIndex"
          :key="column"
          class="header-item"
          :style="{ flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}` }"
          :class="{
            'border-right': showBorder,
          }"
        >
          {{ column.title }}
        </view>
      </view>
    </template>
    ......

    <template v-if="!!fixedRightCol.length">
      <view style="right: 0px" class="fixed">
        <view
          v-for="column in fixedRightCol"
          :id="column.dataIndex"
          :key="column"
          :style="{ flex: `calc(${columns.width}) / ${fixedRightColWidthStr}` }"
          class="header-item"
          :class="{
            'border-right': showBorder,
          }"
        >
          {{ column.title }}
        </view>
      </view>
    </template>
  </view>

  <!-- 表身 -->
  <view
    v-for="(data, index) in dataSource"
    :key="data"
    class="content-col grid"
    :style="colStyle"
  >
    <template v-if="!!fixedLeftCol.length">
      <view style="left: 0px" class="fixed">
        <view
          v-for="dataColumn in fixedLeftCol"
          :key="`${index}${dataColumn.dataIndex}fixed`"
          :style="{ flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}` }"
          class="col-item table-font"
          :class="{
            'zebra-color': index % 2 !== 0,
            'border-right': showBorder,
          }"
        >
          {{ data[dataColumn.dataIndex] }}
        </view>
      </view>
    </template>
    ......

    <template v-if="!!fixedRightCol.length">
      <view style="right: 0px" class="fixed">
        <view
          v-for="dataColumn in fixedRightCol"
          :key="`${index}${dataColumn.dataIndex}fixed`"
          :style="{ flex: `calc(${columns.width}) / ${fixedRightColWidthStr}` }"
          class="col-item table-font"
          :class="{
            'zebra-color': index % 2 !== 0,
            'border-right': showBorder,
          }"
        >
          {{ data[dataColumn.dataIndex] }}
        </view>
      </view>
    </template>
  </view>
  ......
</template>

添加粘性定位,flex弹性布局等css样式属性。

.fixed {
  position: sticky;
  height: inherit;

  display: flex;

  & > view {
    flex: 1;
  }
}

通过js动态计算出父容器宽度。

const fixedLeftCol = props.columns.filter((i) => i.fixed === "left");
const fixedLeftColWidthArr = fixedLeftCol.map((item) => {
  return item.width ? item.width : "1fr";
});
const fixedLeftColWidthStr = `calc(${fixedLeftColWidthArr.join(" + ")})`;

const fixedRightCol = props.columns.filter((i) => i.fixed === "right");
const fixedRightColWidthArr = fixedRightCol.map((item) => {
  return item.width ? item.width : "1fr";
});
const fixedRightColWidthStr = `calc(${fixedRightColWidthArr.join(" + ")})`;

const setWidth = () => {
  const normalcolWidth = props.columns
    .filter((i) => !i.fixed)
    .map((item) => {
      return item.width ? item.width : "1fr";
    });

  let colWidth = [];
  !!fixedLeftCol.length && colWidth.push(fixedLeftColWidthStr);
  colWidth.push(...normalcolWidth);
  !!fixedRightCol.length && colWidth.push(fixedRightColWidthStr);

  return colWidth.join(" ");
};

代码

<template>
  <view
    class="table"
    :class="{
      'border-left': showBorder,
    }"
  >
    <view class="table-header grid" :style="colStyle">
      <template v-if="!!fixedLeftCol.length">
        <view style="left: 0px" class="fixed">
          <view
            v-for="column in fixedLeftCol"
            :id="column.dataIndex"
            :key="column"
            class="header-item"
            :style="{
              flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}`,
            }"
            :class="{
              'border-right': showBorder,
            }"
          >
            {{ column.title }}
          </view>
        </view>
      </template>
      <view
        v-for="column in columns.filter((i) => !i.fixed)"
        :id="column.dataIndex"
        :key="column"
        class="header-item"
        :class="{
          'border-right': showBorder,
        }"
      >
        {{ column.title }}
      </view>
      <template v-if="!!fixedRightCol.length">
        <view style="right: 0px" class="fixed">
          <view
            v-for="column in fixedRightCol"
            :id="column.dataIndex"
            :key="column"
            :style="{
              flex: `calc(${columns.width}) / ${fixedRightColWidthStr}`,
            }"
            class="header-item"
            :class="{
              'border-right': showBorder,
            }"
          >
            {{ column.title }}
          </view>
        </view>
      </template>
    </view>
    <view class="table-content">
      <view
        v-for="(data, index) in dataSource"
        :key="data"
        class="content-col grid"
        :style="colStyle"
      >
        <template v-if="!!fixedLeftCol.length">
          <view style="left: 0px" class="fixed">
            <view
              v-for="dataColumn in fixedLeftCol"
              :key="`${index}${dataColumn.dataIndex}fixed`"
              :style="{
                flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}`,
              }"
              class="col-item table-font"
              :class="{
                'zebra-color': index % 2 !== 0,
                'border-right': showBorder,
              }"
            >
              {{ data[dataColumn.dataIndex] }}
            </view>
          </view>
        </template>
        <view
          v-for="dataColumn in columns.filter((i) => !i.fixed)"
          :key="`${index}${dataColumn.dataIndex}`"
          class="col-item table-font"
          :class="{
            'zebra-color': index % 2 !== 0,
            'border-right': showBorder,
          }"
        >
          {{ data[dataColumn.dataIndex] }}
        </view>
        <template v-if="!!fixedRightCol.length">
          <view style="right: 0px" class="fixed">
            <view
              v-for="dataColumn in fixedRightCol"
              :key="`${index}${dataColumn.dataIndex}fixed`"
              :style="{
                flex: `calc(${columns.width}) / ${fixedRightColWidthStr}`,
              }"
              class="col-item table-font"
              :class="{
                'zebra-color': index % 2 !== 0,
                'border-right': showBorder,
              }"
            >
              {{ data[dataColumn.dataIndex] }}
            </view>
          </view>
        </template>
      </view>
      <view v-if="dataSource.length === 0 && showEmpty" class="tips">
        <image class="tips-img" src="./list.png" mode="aspectFit" />
        <view class="tips-text"> 暂无数据 </view>
      </view>
    </view>
  </view>
</template>

<script>
import { computed } from "vue";

export default {
  props: {
    columns: {
      type: Array,
      default: () => [],
    },
    dataSource: {
      type: Array,
      default: () => [],
    },
    showEmpty: {
      type: Boolean,
      default: true,
    },
    showBorder: {
      type: Boolean,
      default: false,
    },
  },
  setup(props) {
    const colStyle = computed(() => {
      return {
        "grid-template-columns": setWidth(),
      };
    });

    const fixedLeftCol = props.columns.filter((i) => i.fixed === "left");
    const fixedLeftColWidthArr = fixedLeftCol.map((item) => {
      return item.width ? item.width : "1fr";
    });
    const fixedLeftColWidthStr = `calc(${fixedLeftColWidthArr.join(" + ")})`;

    const fixedRightCol = props.columns.filter((i) => i.fixed === "right");
    const fixedRightColWidthArr = fixedRightCol.map((item) => {
      return item.width ? item.width : "1fr";
    });
    const fixedRightColWidthStr = `calc(${fixedRightColWidthArr.join(" + ")})`;

    const setWidth = () => {
      const normalcolWidth = props.columns
        .filter((i) => !i.fixed)
        .map((item) => {
          return item.width ? item.width : "1fr";
        });

      let colWidth = [];
      !!fixedLeftCol.length && colWidth.push(fixedLeftColWidthStr);
      colWidth.push(...normalcolWidth);
      !!fixedRightCol.length && colWidth.push(fixedRightColWidthStr);

      return colWidth.join(" ");
    };

    return {
      colStyle,
      fixedLeftCol,
      fixedLeftColWidthStr,
      fixedRightCol,
      fixedRightColWidthStr,
    };
  },
};
</script>

<style lang="scss">
.table {
  overflow-y: scroll;
  .table-header {
    text-align: center;
    background-color: #f0f3ff;
    .header-item {
      background-color: #f0f3ff;

      color: #4a6cf7;
      font-size: 20px;
      height: 60px;
      line-height: 60px;
      font-weight: 400;
    }
  }
  .table-content {
    width: 100%;

    .content-col {
      width: 100%;
      position: relative;

      .col-item {
        text-align: center;
        // text-overflow: ellipsis;
        white-space: nowrap;
        background-color: #ffffff;
        height: 60px;
        line-height: 60px;
        padding: 0px 10px;
        // flex: 1;
      }
      .zebra-color {
        background-color: #f0f3ff;
      }
    }
    .tips {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 300px;

      .tips-img {
        width: 210px;
        height: 100px;
        margin-bottom: 16px;
      }
      .tips-text {
        font-size: 20px;
        font-weight: 500;
        color: #999999;
      }
    }
  }
}

.grid {
  display: grid;
}
.table-font {
  font-size: 20px;
  color: #333333;
  font-weight: 400;
}
.border-right {
  border-right: 1px solid #e6e6e6;
}
.border-left {
  border-left: 1px solid #e6e6e6;
}
.fixed {
  position: sticky;
  height: inherit;

  display: flex;

  & > view {
    flex: 1;
  }
}
</style>
点赞
收藏
评论区
推荐文章
Easter79 Easter79
3年前
vue+element 表格formatter数据格式化并且插入html标签
前言   vue中element框架,其中表格组件,我既要行内数据格式化,又要插入html标签一贯思维,二者不可兼得也一、element表格数据格式化  !(https://oscimg.oschina.net/oscnet/3c43a1cb3cbdeb5b5ad58acb45a42612b00.p
Easter79 Easter79
3年前
vue element
工作中遇到后台给的表格数据里时间是一个13位的时间戳,需要转换成时间显示在表格里,可以用elementui表格自带的:formatter函数,来格式化表格内容:1//时间戳转换成时间2//使用elementtable组件中的formatter属性,传入一个函数3timestampToTime
APICloud AVM框架封装数据表格组件
用以展示基础表格数据的组件。组件的核心功能点是在数据展示的时候,用到了2个vfor循环,第一层循环是数据对象的循环,然后嵌套列名的对象,通过列名中的key值在数据对象中查询对应的数据,这样就保证了在数据对象与列名对象顺序打乱的情况下也可以把数据对应起来,并能够在列名没有对应的数据的时候进行特殊处理。以APICloudAVM框架封装数据表格组件为例。组件文件
Wesley13 Wesley13
3年前
Java读取word中表格
  因为要新建一个站,公司要把word表格的部分行列存到数据库中。之前用java操作过excel,本来打算用java从word表格中读取数据,再存到数据库中,结果因为权限不够,无法访问公司要写的那个数据库,跪了跪了。  但还是把java读取word中表格的方法写一下,先上代码。publicstaticvoidtestWord(Strin
Stella981 Stella981
3年前
Angular之自定义组件添加默认样式
Angular的核心思想之一就是:组件化。组件化可以使我们的代码更好的复用。在使用官方提供的Angular库AngularMaterial时,细心的同学就会发现,Material的每一个组件都有它自己样式,如:按钮:matbutton工具条:mattoolbar表格
Stella981 Stella981
3年前
React 表格组件 GridManager
GridManagerReact\基于React的GridManager封装,用于便捷的在React中使用GridManager.除过React特性外,其它API与GridManagerAPI相同。!image(https://s2.ax1x.com/2019/04/16/AxA4xK.
Python进阶者 Python进阶者
1年前
盘点一个Python自动化办公需求,实现数据自动填充
大家好,我是皮皮。一、前言前几天遇到了一个小需求,粉丝自己在实际工作中的需求,需要把下图的表格内容,自动填充到目标表格中去,省得挨个去复制粘贴了,而且还十分容易出错。原始表格如下图所示:目标表格如下图所示:二、实现过程这里【枫涧澈浪】大佬给了一个代码,如下
为React Ant-Design Table增加字段设置 | 京东云技术团队
最近做的几个项目经常遇到这样的需求,要在表格上增加一个自定义表格字段设置的功能。就是用户可以自己控制那些列需要展示。在几个项目里都实现了一遍,每个项目的需求又都有点儿不一样,迭代了很多版,所以抽时间把这个功能封装了个组件:,将这些差别都集成了进去,方便今后
Python进阶者 Python进阶者
8个月前
盘点一个Pandas实战需求的问题
大家好,我是Python进阶者。一、前言前几天在Python最强王者交流群【wen】问了一个Pandas解决实际需求的实战问题。问题如下:请教:代码的目的为自动填充产品名字,有多个销售数据的表格,如例子,销售数据表格中的的产品名字一列为空,我把销售数据表格
sum墨 sum墨
1个月前
《优化接口设计的思路》系列:第十一篇—表格的导入导出接口优化
在后端开发中,我们经常处理增删改查的接口,这些操作已经非常熟悉了。然而,有时产品经理会要求增加一个表格数据的导入和导出功能,让用户可以离线处理数据。这类操作也很常见,但处理起来不太简单。尽管一些前端表格组件可以直接实现这类功能,但往往不够灵活,因为前端的数据通常已经过处理。如果要获取原始数据,还是得依靠后端处理。