前言
工作中接到一个小程序表格展示信息的需求,让我一惊,因为Taro UI Vue3
这个框架根本没有表格组件。
没办法咯,只能自己写了,因为没写过表格组件,心里还是有点畏惧的。但是写完以后,我发现在这个gird
网格布局大行其道的时代,编写一个实现基本功能的表格组件并不难。
特别是有很多成功的框架珠玉在前,比如Antd
,所以我仿照Antd
的表格组件实现了一个青春版。
给阅读这篇博客的人提个醒,我没看过Antd
源码,以下实现代码是我自己写的。😂
需求
- 表格的结构和数据分离
- 默认表格宽度根据容器宽度均分,有
width
属性使用自定义宽度 - 无数据提示
- 控制表格边框展示
- 斑马纹
实现
释义
讲一下我用到的不太常见的属性。
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,
},
}
}
最后我们来看看最终实现效果。
拓展
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>