尝试用 Vue 写一个表格组件

前言

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

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

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

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

需求

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

实现

释义

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

1. display: grid

设置元素为网格布局。

2. grid-template-columns

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

思路

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

1export default { 2 ...... 3 props: { 4 columns: { 5 type: Array, 6 default: () => [], 7 }, 8 dataSource: { 9 type: Array, 10 default: () => [], 11 }, 12 } 13}

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

1<template> 2 ...... 3 4 <view class="table-header grid" :style="colStyle"> 5 <view 6 v-for="column in columns" 7 :id="column.dataIndex" 8 :key="column" 9 class="header-item" 10 > 11 {{ column.title }} 12 </view> 13 </view> 14 <view class="table-content"> 15 <view 16 v-for="(data, index) in dataSource" 17 :key="data" 18 class="content-col grid" 19 :style="colStyle" 20 > 21 <view 22 v-for="dataColumn in columns" 23 :key="index + dataColumn.dataIndex" 24 class="col-item table-font" 25 > 26 {{ data[dataColumn.dataIndex] }} 27 </view> 28 </view> 29 </view> 30 ...... 31</template>

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

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

1const colStyle = computed(() => { 2 const number = props.columns.length; 3 return { 4 "grid-template-columns": setWidth(), 5 }; 6}); 7 8const setWidth = () => { 9 return props.columns 10 .map((item) => { 11 return item.width ? item.width : "1fr"; 12 }) 13 .join(" "); 14};

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

1<template> 2 <!-- showBorder 边框 --> 3 <view 4 class="table" 5 :class="{ 6 'border-left': showBorder, 7 }" 8 > 9 <view class="table-header grid" :style="colStyle"> 10 <view 11 v-for="column in columns" 12 :id="column.dataIndex" 13 :key="column" 14 class="header-item" 15 :class="{ 16 'border-right': showBorder, 17 }" 18 > 19 {{ column.title }} 20 </view> 21 </view> 22 <view class="table-content"> 23 <view 24 v-for="(data, index) in dataSource" 25 :key="data" 26 class="content-col grid" 27 :style="colStyle" 28 > 29 <!-- zebra-color 斑马纹 --> 30 <view 31 v-for="dataColumn in columns" 32 :key="index + dataColumn.dataIndex" 33 class="col-item table-font" 34 :class="{ 35 'zebra-color': index % 2 !== 0, 36 'border-right': showBorder, 37 }" 38 > 39 {{ data[dataColumn.dataIndex] }} 40 </view> 41 </view> 42 <!-- 无数据提示 --> 43 <view v-if="dataSource.length === 0 && showEmpty" class="tips"> 44 <image class="tips-img" src="./list.png" mode="aspectFit" /> 45 <view class="tips-text"> 暂无数据 </view> 46 </view> 47 </view> 48 </view> 49</template>
1export default { 2 ...... 3 props: { 4 ...... 5 showEmpty: { 6 type: Boolean, 7 default: true, 8 }, 9 showBorder: { 10 type: Boolean, 11 default: false, 12 }, 13 } 14}

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

表格

表格

拓展

fixed 属性

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

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

方案

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

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

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

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

最后选择方案 2

问题

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

1. 元素堆叠

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

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

2. 宽度失准

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

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

具体代码

判断左右,控制组件展示

1<!-- 表头 --> 2<template> 3 ...... 4 5 <view class="table-header grid" :style="colStyle"> 6 <template v-if="!!fixedLeftCol.length"> 7 <view style="left: 0px" class="fixed"> 8 <view 9 v-for="column in fixedLeftCol" 10 :id="column.dataIndex" 11 :key="column" 12 class="header-item" 13 :style="{ flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}` }" 14 :class="{ 15 'border-right': showBorder, 16 }" 17 > 18 {{ column.title }} 19 </view> 20 </view> 21 </template> 22 ...... 23 24 <template v-if="!!fixedRightCol.length"> 25 <view style="right: 0px" class="fixed"> 26 <view 27 v-for="column in fixedRightCol" 28 :id="column.dataIndex" 29 :key="column" 30 :style="{ flex: `calc(${columns.width}) / ${fixedRightColWidthStr}` }" 31 class="header-item" 32 :class="{ 33 'border-right': showBorder, 34 }" 35 > 36 {{ column.title }} 37 </view> 38 </view> 39 </template> 40 </view> 41 42 <!-- 表身 --> 43 <view 44 v-for="(data, index) in dataSource" 45 :key="data" 46 class="content-col grid" 47 :style="colStyle" 48 > 49 <template v-if="!!fixedLeftCol.length"> 50 <view style="left: 0px" class="fixed"> 51 <view 52 v-for="dataColumn in fixedLeftCol" 53 :key="`${index}${dataColumn.dataIndex}fixed`" 54 :style="{ flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}` }" 55 class="col-item table-font" 56 :class="{ 57 'zebra-color': index % 2 !== 0, 58 'border-right': showBorder, 59 }" 60 > 61 {{ data[dataColumn.dataIndex] }} 62 </view> 63 </view> 64 </template> 65 ...... 66 67 <template v-if="!!fixedRightCol.length"> 68 <view style="right: 0px" class="fixed"> 69 <view 70 v-for="dataColumn in fixedRightCol" 71 :key="`${index}${dataColumn.dataIndex}fixed`" 72 :style="{ flex: `calc(${columns.width}) / ${fixedRightColWidthStr}` }" 73 class="col-item table-font" 74 :class="{ 75 'zebra-color': index % 2 !== 0, 76 'border-right': showBorder, 77 }" 78 > 79 {{ data[dataColumn.dataIndex] }} 80 </view> 81 </view> 82 </template> 83 </view> 84 ...... 85</template>

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

1.fixed { 2 position: sticky; 3 height: inherit; 4 5 display: flex; 6 7 & > view { 8 flex: 1; 9 } 10}

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

1const fixedLeftCol = props.columns.filter((i) => i.fixed === "left"); 2const fixedLeftColWidthArr = fixedLeftCol.map((item) => { 3 return item.width ? item.width : "1fr"; 4}); 5const fixedLeftColWidthStr = `calc(${fixedLeftColWidthArr.join(" + ")})`; 6 7const fixedRightCol = props.columns.filter((i) => i.fixed === "right"); 8const fixedRightColWidthArr = fixedRightCol.map((item) => { 9 return item.width ? item.width : "1fr"; 10}); 11const fixedRightColWidthStr = `calc(${fixedRightColWidthArr.join(" + ")})`; 12 13const setWidth = () => { 14 const normalcolWidth = props.columns 15 .filter((i) => !i.fixed) 16 .map((item) => { 17 return item.width ? item.width : "1fr"; 18 }); 19 20 let colWidth = []; 21 !!fixedLeftCol.length && colWidth.push(fixedLeftColWidthStr); 22 colWidth.push(...normalcolWidth); 23 !!fixedRightCol.length && colWidth.push(fixedRightColWidthStr); 24 25 return colWidth.join(" "); 26};

代码

1<template> 2 <view 3 class="table" 4 :class="{ 5 'border-left': showBorder, 6 }" 7 > 8 <view class="table-header grid" :style="colStyle"> 9 <template v-if="!!fixedLeftCol.length"> 10 <view style="left: 0px" class="fixed"> 11 <view 12 v-for="column in fixedLeftCol" 13 :id="column.dataIndex" 14 :key="column" 15 class="header-item" 16 :style="{ 17 flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}`, 18 }" 19 :class="{ 20 'border-right': showBorder, 21 }" 22 > 23 {{ column.title }} 24 </view> 25 </view> 26 </template> 27 <view 28 v-for="column in columns.filter((i) => !i.fixed)" 29 :id="column.dataIndex" 30 :key="column" 31 class="header-item" 32 :class="{ 33 'border-right': showBorder, 34 }" 35 > 36 {{ column.title }} 37 </view> 38 <template v-if="!!fixedRightCol.length"> 39 <view style="right: 0px" class="fixed"> 40 <view 41 v-for="column in fixedRightCol" 42 :id="column.dataIndex" 43 :key="column" 44 :style="{ 45 flex: `calc(${columns.width}) / ${fixedRightColWidthStr}`, 46 }" 47 class="header-item" 48 :class="{ 49 'border-right': showBorder, 50 }" 51 > 52 {{ column.title }} 53 </view> 54 </view> 55 </template> 56 </view> 57 <view class="table-content"> 58 <view 59 v-for="(data, index) in dataSource" 60 :key="data" 61 class="content-col grid" 62 :style="colStyle" 63 > 64 <template v-if="!!fixedLeftCol.length"> 65 <view style="left: 0px" class="fixed"> 66 <view 67 v-for="dataColumn in fixedLeftCol" 68 :key="`${index}${dataColumn.dataIndex}fixed`" 69 :style="{ 70 flex: `calc(${columns.width}) / ${fixedLeftColWidthStr}`, 71 }" 72 class="col-item table-font" 73 :class="{ 74 'zebra-color': index % 2 !== 0, 75 'border-right': showBorder, 76 }" 77 > 78 {{ data[dataColumn.dataIndex] }} 79 </view> 80 </view> 81 </template> 82 <view 83 v-for="dataColumn in columns.filter((i) => !i.fixed)" 84 :key="`${index}${dataColumn.dataIndex}`" 85 class="col-item table-font" 86 :class="{ 87 'zebra-color': index % 2 !== 0, 88 'border-right': showBorder, 89 }" 90 > 91 {{ data[dataColumn.dataIndex] }} 92 </view> 93 <template v-if="!!fixedRightCol.length"> 94 <view style="right: 0px" class="fixed"> 95 <view 96 v-for="dataColumn in fixedRightCol" 97 :key="`${index}${dataColumn.dataIndex}fixed`" 98 :style="{ 99 flex: `calc(${columns.width}) / ${fixedRightColWidthStr}`, 100 }" 101 class="col-item table-font" 102 :class="{ 103 'zebra-color': index % 2 !== 0, 104 'border-right': showBorder, 105 }" 106 > 107 {{ data[dataColumn.dataIndex] }} 108 </view> 109 </view> 110 </template> 111 </view> 112 <view v-if="dataSource.length === 0 && showEmpty" class="tips"> 113 <image class="tips-img" src="./list.png" mode="aspectFit" /> 114 <view class="tips-text"> 暂无数据 </view> 115 </view> 116 </view> 117 </view> 118</template> 119 120<script> 121import { computed } from "vue"; 122 123export default { 124 props: { 125 columns: { 126 type: Array, 127 default: () => [], 128 }, 129 dataSource: { 130 type: Array, 131 default: () => [], 132 }, 133 showEmpty: { 134 type: Boolean, 135 default: true, 136 }, 137 showBorder: { 138 type: Boolean, 139 default: false, 140 }, 141 }, 142 setup(props) { 143 const colStyle = computed(() => { 144 return { 145 "grid-template-columns": setWidth(), 146 }; 147 }); 148 149 const fixedLeftCol = props.columns.filter((i) => i.fixed === "left"); 150 const fixedLeftColWidthArr = fixedLeftCol.map((item) => { 151 return item.width ? item.width : "1fr"; 152 }); 153 const fixedLeftColWidthStr = `calc(${fixedLeftColWidthArr.join(" + ")})`; 154 155 const fixedRightCol = props.columns.filter((i) => i.fixed === "right"); 156 const fixedRightColWidthArr = fixedRightCol.map((item) => { 157 return item.width ? item.width : "1fr"; 158 }); 159 const fixedRightColWidthStr = `calc(${fixedRightColWidthArr.join(" + ")})`; 160 161 const setWidth = () => { 162 const normalcolWidth = props.columns 163 .filter((i) => !i.fixed) 164 .map((item) => { 165 return item.width ? item.width : "1fr"; 166 }); 167 168 let colWidth = []; 169 !!fixedLeftCol.length && colWidth.push(fixedLeftColWidthStr); 170 colWidth.push(...normalcolWidth); 171 !!fixedRightCol.length && colWidth.push(fixedRightColWidthStr); 172 173 return colWidth.join(" "); 174 }; 175 176 return { 177 colStyle, 178 fixedLeftCol, 179 fixedLeftColWidthStr, 180 fixedRightCol, 181 fixedRightColWidthStr, 182 }; 183 }, 184}; 185</script> 186 187<style lang="scss"> 188.table { 189 overflow-y: scroll; 190 .table-header { 191 text-align: center; 192 background-color: #f0f3ff; 193 .header-item { 194 background-color: #f0f3ff; 195 196 color: #4a6cf7; 197 font-size: 20px; 198 height: 60px; 199 line-height: 60px; 200 font-weight: 400; 201 } 202 } 203 .table-content { 204 width: 100%; 205 206 .content-col { 207 width: 100%; 208 position: relative; 209 210 .col-item { 211 text-align: center; 212 // text-overflow: ellipsis; 213 white-space: nowrap; 214 background-color: #ffffff; 215 height: 60px; 216 line-height: 60px; 217 padding: 0px 10px; 218 // flex: 1; 219 } 220 .zebra-color { 221 background-color: #f0f3ff; 222 } 223 } 224 .tips { 225 display: flex; 226 flex-direction: column; 227 align-items: center; 228 justify-content: center; 229 height: 300px; 230 231 .tips-img { 232 width: 210px; 233 height: 100px; 234 margin-bottom: 16px; 235 } 236 .tips-text { 237 font-size: 20px; 238 font-weight: 500; 239 color: #999999; 240 } 241 } 242 } 243} 244 245.grid { 246 display: grid; 247} 248.table-font { 249 font-size: 20px; 250 color: #333333; 251 font-weight: 400; 252} 253.border-right { 254 border-right: 1px solid #e6e6e6; 255} 256.border-left { 257 border-left: 1px solid #e6e6e6; 258} 259.fixed { 260 position: sticky; 261 height: inherit; 262 263 display: flex; 264 265 & > view { 266 flex: 1; 267 } 268} 269</style>
点赞
收藏

评论区

加载中...

相关推荐

vue+element 表格formatter数据格式化并且插入html标签

前言   vue中element框架,其中表格组件,我既要行内数据格式化,又要插入html标签一贯思维,二者不可兼得也一、element表格数据格式化  !(https://oscimg.oschina.net/oscnet/3c43a1cb3cbdeb5b5ad58acb45a42612b00.p

vue element

工作中遇到后台给的表格数据里时间是一个13位的时间戳,需要转换成时间显示在表格里,可以用elementui表格自带的:formatter函数,来格式化表格内容:1//时间戳转换成时间2//使用elementtable组件中的formatter属性,传入一个函数3timestampToTime

APICloud AVM框架封装数据表格组件

用以展示基础表格数据的组件。组件的核心功能点是在数据展示的时候,用到了2个vfor循环,第一层循环是数据对象的循环,然后嵌套列名的对象,通过列名中的key值在数据对象中查询对应的数据,这样就保证了在数据对象与列名对象顺序打乱的情况下也可以把数据对应起来,并能够在列名没有对应的数据的时候进行特殊处理。以APICloudAVM框架封装数据表格组件为例。组件文件

Java读取word中表格

  因为要新建一个站,公司要把word表格的部分行列存到数据库中。之前用java操作过excel,本来打算用java从word表格中读取数据,再存到数据库中,结果因为权限不够,无法访问公司要写的那个数据库,跪了跪了。  但还是把java读取word中表格的方法写一下,先上代码。publicstaticvoidtestWord(Strin

Angular之自定义组件添加默认样式

Angular的核心思想之一就是:组件化。组件化可以使我们的代码更好的复用。在使用官方提供的Angular库AngularMaterial时,细心的同学就会发现,Material的每一个组件都有它自己样式,如:按钮:matbutton工具条:mattoolbar表格

React 表格组件 GridManager

GridManagerReact\基于React的GridManager封装,用于便捷的在React中使用GridManager.除过React特性外,其它API与GridManagerAPI相同。!image(https://s2.ax1x.com/2019/04/16/AxA4xK.