本文字数:3520字
预计阅读时间:20分钟
所谓动态列的表格,就是列数不固定。像广为使用的elementUI的table组件就是表头写死的,这种也叫列数固定的表格。
01
效果
当然,动态性增加了,当然要做出一定“牺牲”。这是表格组件的表头和表内容的数据格式不太一样了——我们把它分为两个数组传入:
02
数据传入
columns: [ // 表头
{ title: 'Full Name', width: 132, dataIndex: 'name', fixed: 'left' },
{ title: 'Age', width: 100, dataIndex: 'age' },
{ title: 'address1', dataIndex: 'address1', key: '1', width: 150 },
{ title: 'address2', dataIndex: 'address2', key: '2', width: 150 },
//...
{ title: '操作', dataIndex: 'do', width: 172, fixed: 'right' }
],
data: [
{ key: 1, name: '章三', age: '18', class: '2班', address1: '111', address2: '222', address3: '333', address4: '444', address5: '555', address6: '666', address7: '777', isEdit: false },
{ key: 2, name: '章三2', age: '18', class: '2班', address1: '111', address2: '222', address3: '333', address4: '444', address5: '555', address6: '666', address7: '777', isEdit: false }
]
可以看到,“表头”数组中的title属性就是表头应该展示的内容,dataIndex属性就是和“表内容”data数组中关联的属性。它的值如果作为key出现在表内容数组中,则表内容这一项会展示在表格中,反之则不会。
这样的数据格式也是方便了动态的特点:前端可以根据特定的场景对表头和内容单独分别处理、二次开发比如“checkbox”也可以针对数据格式做校验。
这里也是为另一种情况考虑:表头和表内容数组都由后端提供,并不是所有返回的东西都要展示,也不是没有展示的东西都不需要,比如某一行数据的修改需要id—— 数据由后端提供,样式由前端修改。
我们继续分析数据:我们还看到了fixed属性和width属性。前者是用来判断超出表格宽度时最左侧和最右侧是否固定在两侧,这个属性只能在表头数组的第一项和最后一项中出现后者是控制当前列的宽度。这个属性也只能在表头数组中出现!而表内容数组中出现了另一个值:isEdit。它用来判断当前行是否“在修改”。后面会看到,我们给表内容的每一项v-if了一个input或者自定义component。
03
基础版实现
表格整体当然是用了原生的table、tr、td实现。
虽然表头看似是一个单独的内容,但是为了样式考虑,我们并没有放在th中,而是作为一个普通的td,反之样式可以自定义:
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
<table>
<colgroup>
<col v-for="(column, index) in columns" :key="index"
:style="{ width: column.width + 'px', minWidth: column.width + 'px' }"
:class="{ 'fixed-left': index === 0, 'fixed-right': index === columns.length - 1 && column.fixed === 'right' }" />
</colgroup>
<tbody>
<tr>
<td v-for="(column, index) in columns" :key="index"
:style="{ width: column.width + 'px', minWidth: column.width + 'px' }"
:class="{ 'fixed-left': index === 0, 'fixed-right': index === columns.length - 1 && column.fixed === 'right', 'header-cell': true }">
<div class="fixed-item"><div style="display: flex;align-items: center;height: 22px;line-height: 22px;">{{ column.title }}</div></div>
</td>
</tr>
<tr v-for="(row, rowIndex) in data" :key="rowIndex">
<td v-for="(column, columnIndex) in columns" :key="columnIndex"
:class="{ 'fixed-left': columnIndex === 0, 'fixed-right': columnIndex === columns.length - 1 && column.fixed === 'right' }">
<div class="fixed-item">
<template v-if="column.dataIndex === 'do'">
<div style="display: flex;align-items: center;height: 22px;line-height: 22px;">
<slot :row="row"></slot>
</div>
</template>
<template v-else-if="!row.isEdit && !row.component"><div style="display: flex;align-items: center;height: 22px;line-height: 22px;">{{ row[column.dataIndex] }}</div></template>
<component :is="row.component" v-bind="row.props" v-else-if="row.component" />
<template v-else>
<div style="display: flex;align-items: center;">
<a-input v-model="row[column.dataIndex]" placeholder="" allow-clear />
</div>
</template>
</div>
</td>
</tr>
</tbody>
</table>
</div>
这操作看着很常规:
在表格的HTML结构中,使用v-for指令来循环生成列和行。v-for="(column, index)in columns"用于生成列,v-for="(row, rowIndex)in data"用于生成行;
每个单元格的内容由row[column.dataIndex]决定,其中column.dataIndex是列的属性名,row是当前行的数据对象。
为了简化代码和防止数据冲突,我用了<colgroup>和<col>标签,以达到“只需要在表头数据中添加width即可”的效果。从性能角度考虑:使用<colgroup>和<col>元素也可以帮助浏览器更有效地渲染表格。由于列的宽度和样式是在<col>元素中定义的,浏览器可以提前计算表格的布局,从而提高渲染性能!
.table-container {
overflow-x: auto;
max-width: 100%;
position: relative;
td {
padding: 0;
background-color: #fff;
border-bottom: 0.9px solid #eee;
.fixed-item {
padding: 13px;
&.header-cell {
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
}
}
}
}
.fixed-left {
position: sticky;
left: 0;
width: 142px;
align-items: center;
z-index: 9;
.fixed-item {
display: block;
}
}
.fixed-right {
position: sticky;
right: 0;
width: 172px;
align-items: center;
z-index: 9;
.fixed-item {
display: block;
}
}
.header-cell {
background-color: #fafafa !important;
}
同时,监听了表格的scroll事件,在滚动的时候动态添加删除某个元素 —— 让表格左右侧列的阴影效果在需要的时候才展示:
handleScroll(event) {
const container = event.target;
const scrollLeft = container.scrollLeft;
const maxScrollLeft = container.scrollWidth - container.clientWidth;
// 根据滚动位置添加或移除阴影样式
if (scrollLeft === 0) {
container.classList.add('scroll-left');
container.classList.remove('scroll-right');
} else if (scrollLeft >= maxScrollLeft) {
container.classList.add('scroll-right');
container.classList.remove('scroll-left');
} else {
container.classList.add('scroll-left');
container.classList.add('scroll-right');
}
}
对应的css样式:
/* 添加阴影样式 */
&.scroll-left .fixed-right {
border-bottom: 0.1px solid transparent !important;
.fixed-item {
width: 100%;
height: 100%;
box-shadow: 1px 57px 22px 0 rgba(0, 0, 0, 0.2);
}
}
&.scroll-right .fixed-left {
border-bottom: 0.1px solid transparent !important;
.fixed-item {
width: 100%;
height: 100%;
box-shadow: -1px 57px 22px 0 rgba(0, 0, 0, 0.2);
}
}
到此为止,如开头所示就实现了。
使用如下:
<biaoge :columns="columns" :data="data">
<template v-slot:default="{ row }">
<a-button style="height: 22px;line-height: 22px;" type="link" @click="toggleEdit(row)">{{ row.isEdit ? '完成' : '修改' }}</a-button>
</template>
</biaoge>
04
进阶?
上面的代码虽然我们只在滚动中操作了class,并没有直接操控 style,但它仍然是监听了scroll。带来了很大的性能隐患。
能不能完全用css实现阴影的动态显示?能!
什么是“阴影动态显示”?在表格内容超出可视区域左右滚动时对超出部分有阴影提示效果。
4.1
在CSS3的时代,我们可以在想要加滚动条的地方外包裹一层div,为其设置overflow:hidden,内部用calc()函数动态计算width使其溢出。这可以有效解决IE下兼容性问题。我们现在已经很少通过滚动条来滚动页面了(更多的是使用触摸手势),但滚动条对于元素内容可滚动的提示作用仍然是十分有用的,哪怕对于那些没有发生交互的元素也是如此;而且这种提示方式十分巧妙。
假如有一个ul、li列表:
<ul>
<li>Ada Catlace</li>
<li>Alan Purring</li>
<li>Schrödingcat</li>
<li>Tim Purrners-Lee</li>
<li>WebKitty</li>
<li>WebKitty</li>
<li>Json</li>
<li>Void</li>
</ul>
对 ul 来说:
overflow: auto;
width: 10em;
height: 8em;
padding: .3em .5em;
border: 1px solid silver;
我们用一个径向渐变在顶部添加一条阴影:
background: radial-gradient(at top, rgba(0,0,0,.2),transparent 70%) no-repeat;
background-size: 100% 15px;
现在,当我们滚动列表时,这条阴影会一直停留在相同的位置。这正是背景图像的默认行为:它的位置是相对于元素固定的!不论元素的内容是否发生了滚动。这一点也适用于background-attachment: fixed的背景图像。它们唯一的区别是,当页面滚动时,后者是相对于视口固定的。有没有办法让背景图像跟着元素的内容一起滚动呢?
现在常见的值只有inherit、scroll、fixed,但是从W3C文档中可以看到:后来为background-attachment属性增加了一个新的关键字,叫作local。如果将此属性应用到这条阴影上,它会带给我们正好相反的效果:当我们滚动到最顶端时,能看到一条阴影;但当我们向下滚动时,这条阴影就消失了。
我想到了一个很常用的hack:我们需要两层背景:一层用来生成那条阴影,另一层基本上就是一个用来遮挡阴影的白色矩形,其作用类似于遮罩层。生成阴影的那层背景将具有默认的 background-attachment值(scroll),因为我们希望它总是保持在原位。我们把遮罩背景的background-attachment属性设置为local,这样它就会在我们滚动到最顶部时盖住阴影,在向下滚动时跟着滚动,从而露出阴影。
background: linear-gradient(white 30%, transparent),
radial-gradient(at 50% 0, rgba(0,0,0,.2),transparent 70%);
background-repeat: no-repeat;
background-size: 100% 50px, 100% 15px;
background-attachment: local, scroll;
下方的阴影只需要添加*-gradient的第一个参数,改变方向即可 —— 我们的表格组件也可以这样写:
background: linear-gradient(to right,white 30%, transparent) left / 100% 50px,
radial-gradient(at 0 50%, rgba(0,0,0,.2),transparent 72%) left / 100% 15px,
linear-gradient(to left, white 15px, hsla(0,0%,100%,0)) right / 100% 50px,
radial-gradient(at right, rgba(0,0,0,.2), transparent 72%) right / 100% 15px;
background-repeat: no-repeat;
background-attachment: scroll, local, scroll, local;