Vue手搓轮子系列——表格封装(二)
概述
本文在Vue手搓轮子系列——表格封装(一) 的基础上增加了排序、展开功能。主要通过中间件实现(如果一层中间件不够,就再加一层)。
代码
LiteTabel.vue
<template>
<div class="table">
<table cellspacing="0" class="lite-table" :class="border ? 'is-border' : ''">
<thead>
<tr>
<lite-table-column-warp column-type="header" :sort-status="sortstatus" @sort="sort">
<slot />
</lite-table-column-warp>
</tr>
</thead>
<tbody>
<lite-table-row ref="table" :row-class-name="rowClassName" :data="tableData" :default-expand-all="defaultExpandAll" :row-key="rowKey" :expand-row-keys="expandRowKeys">
<slot />
</lite-table-row>
<!-- <tr v-for="(row, index) in data" :key="row">
<lite-table-column-warp column-type="body" :row="row" :index="index">
<slot />
</lite-table-column-warp>
</tr> -->
</tbody>
</table>
<div v-if="!tableData.length" :class="border ? 'lite-empty is-border' : ''">
{{emptyText}}
</div>
</div>
</template>
<script>
import LiteTableColumnWarp from './LiteTableColumnWarp.vue';
import LiteTableRow from './LiteTableRow.vue';
import { getDeepObjectValue } from '../utils';
export default {
name: 'LiteTable',
components: {
LiteTableColumnWarp,
LiteTableRow
},
data() {
return {
sortstatus:{ // 排序状态
prop: '', // 排序key
order: '' // 正序or倒序
}
}
},
methods: {
sort(prop, order){
this.sortstatus = {
prop,
order
}
},
toggleRowExpansion(row, expanded) {
this.$refs.table.toggleRowExpansion(row, expanded);
}
},
props: {
data: {
type: Array,
required: true
},
rowKey: {
type: String,
default: ''
},
expandRowKeys: {
type: Array,
default: () => []
},
defaultExpandAll: {
type: Boolean,
default: false
},
rowClassName: {
type: Function,
default: () => ''
},
border: {
type: Boolean,
default: false
},
emptyText: {
type: String,
default: '暂无数据'
}
},
computed: {
tableData() {
try{
if(this.sortstatus.order == 'ascending'){
let data = this.data.map(item => item);
data.sort((a, b) => {
let aVal = getDeepObjectValue(a, this.sortstatus.prop);
let bVal = getDeepObjectValue(b, this.sortstatus.prop);
if(aVal > bVal){
return 1;
}else if(aVal < bVal){
return -1;
}else{
return 0;
}
});
return data;
}else if(this.sortstatus.order == 'descending'){
let data = this.data.map(item => item);
data.sort((a, b) => {
let aVal = getDeepObjectValue(a, this.sortstatus.prop);
let bVal = getDeepObjectValue(b, this.sortstatus.prop);
if(aVal > bVal){
return -1;
}else if(aVal < bVal){
return 1;
}else{
return 0;
}
});
return data;
}else{
return this.data;
}
}catch(e) {
return this.data;
}
}
}
}
</script>
<style>
.lite-table{
border-spacing: 0;
border-collapse: collapse;
width: 100%;
table-layout: fixed;
}
.lite-table th, .lite-table td{
padding-top: 12px;
padding-bottom: 12px;
}
.is-border th, .is-border td{
border: 1px solid #ebeef5;
}
.lite-empty{
padding-top:12px;
padding-bottom:12px;
text-align:center;
color: var(--el-color-text-secondary); /*element-plus 颜色,请按需修改*/
}
.lite-empty.is-border{
border-left: 1px solid #ebeef5;
border-right: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
}
</style>
在这里重新增加了一个vue组件中间层lite-table-row。主要原因是,将行展开以后,是一种递归的结构,而直接将lite-table进行递归会出现table的表头重复的问题,因此这里重新增加一个lite-table-row中间件。
LiteTableRow.vue
<template>
<template v-for="(row, index) in data" :key="row">
<tr :class="rowClassName(row)">
<lite-table-column-warp :space="space" :is-expand="isExpanded(row, index)" column-type="body" :row="row" :index="index" @expand="expand_index[index] = !expand_index[index]">
<slot/>
</lite-table-column-warp>
</tr>
<LiteTableRow :space="space+1" :row-class-name="rowClassName" ref="table" v-if="hasChildren(row) && isExpanded(row, index)" :default-expand-all="defaultExpandAll" :data="row.children" :row-key="rowKey" :expand-row-keys="expandRowKeys" >
<slot/>
</LiteTableRow>
</template>
</template>
<script>
import LiteTableColumnWarp from './LiteTableColumnWarp.vue';
export default {
components: {
LiteTableColumnWarp
},
name: 'LiteTableRow',
data() {
return {
expand_index:[],
}
},
props: {
data: {
type: Array,
required: true
},
rowKey: {
type: String,
default: ''
},
expandRowKeys: {
type: Array,
default: () => []
},
defaultExpandAll: {
type: Boolean,
default: false
},
rowClassName:{
type: Function,
default: () => ''
},
space: {
type: Number,
default: 0
}
},
methods: {
hasChildren(row) {
return row.children && row.children.length > 0;
},
isExpanded(row, index) {
// if (this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0) {
// let ret = this.expand_index[index] &&
// (this.expandRowKeys.some(item => item==row[this.rowKey]) ||
// row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey])));
// return ret;
// }
return this.expand_index[index];
},
toggleRowExpansion(row, expanded) {
let index = this.data.indexOf(row);
if(index != -1){
if (expanded !== undefined) {
this.expand_index[index] = expanded;
} else {
this.expand_index[index] = !this.expand_index[index];
}
return;
}
this.data.forEach((item, i)=>{
if(item.children && item.children.length > 0){
if(item.children.indexOf(row) != -1){
this.expand_index[i] = true;
}
}
})
this.$nextTick(()=>{
if(this.$refs.table){
if(Array.isArray(this.$refs.table)){
this.$refs.table.forEach(item => {
item.toggleRowExpansion(row, expanded);
});
}else{
this.$refs.table.toggleRowExpansion(row, expanded);
}
}
})
}
},
watch: {
defaultExpandAll(val){
if(val){
this.expand_index = this.data.map(() => true);
}else{
this.expand_index = this.data.map((row)=>{
if(this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0){
return this.expandRowKeys.some(item => item==row[this.rowKey]) ||
row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey]));
}
return false;
});
}
},
data:{
handler(val){
if(this.defaultExpandAll){
this.expand_index = val.map(() => true);
}else{
this.expand_index = val.map((row)=>{
if(this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0){
return this.expandRowKeys.some(item => item==row[this.rowKey]) ||
row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey]));
}
return false;
});
}
},
deep: true
}
},
mounted() {
if(this.defaultExpandAll){
this.expand_index = this.data.map(() => true);
}
else{
this.expand_index = this.data.map((row)=>{
if(this.rowKey && this.expandRowKeys.length > 0 && row.children && row.children.length > 0){
return this.expandRowKeys.some(item => item==row[this.rowKey]) ||
row.children.some(item => this.expandRowKeys.some(key => key==item[this.rowKey]));
}
return false;
});
}
}
}
</script>
这里通过expand_index数组与是否存在子节点数据(children)保证是否将行展开。
此外,通过递归来实现子节点。由于树/森林本身就是递归的,因此,这里的子节点也可以用树/森林来进行抽象,从而将该组件进行递归即可获得带有展开行的表格。
LiteTableColumnWarp.vue
<template>
<slot/>
</template>
<script>
export default {
name: 'LiteTableColumnWarp',
props: {
columnType:{
type: String,
default: '',
},
row:{
type: Object,
default: () => {},
},
index:{
type: Number,
default: 0,
},
isExpand:{
type: Boolean,
default: false,
},
sortStatus:{
type: Object,
default: () => {},
},
space:{
type: Number,
default: 0,
}
},
methods: {
expand() {
this.$emit('expand');
},
sort(prop, order){
this.$emit('sort', prop, order);
}
}
}
</script>
该组件主要承担着LiteTableColumn与上层组件LiteTableRow和LiteTable之间的通讯职能。由于使用了slot插槽将LiteTableColumn插入LiteTableRow与LiteTable,而slot不能进行事件监听,因此使用该组件进行通讯。
LiteTableColumn.vue
<template>
<template v-if="columnType == 'header'">
<th :width="width" :style="Style" v-if="sortable" @click="setOrder" class="lite-sort-th">
<div v-if="$slots.header" class="lite-td" :class="thClass">
<slot name="header" />
<span class="lite-caret-wrapper">
<i @click="SetAscending" class="el-icon-caret-top lite-ascending" :style="asc" @click.stop></i>
<i @click="SetDescending" class="el-icon-caret-bottom lite-descending" :style="des" @click.stop></i>
</span>
</div>
<div v-else class="lite-td" :class="thClass">
{{label}}
<span class="lite-caret-wrapper">
<i @click="SetAscending" class="el-icon-caret-top ascending" :style="asc" @click.stop></i>
<i @click="SetDescending" class="el-icon-caret-bottom descending" :style="asc" @click.stop></i>
</span>
</div>
</th>
<th :width="width" :style="Style" v-else>
<div v-if="$slots.header" class="lite-td" :class="thClass">
<slot name="header" />
</div>
<div v-else>
{{label}}
</div>
</th>
</template>
<template v-if="columnType == 'body'">
<td :width="width" :style="Style">
<div class="lite-td" :class="tdClass">
<span v-if="space && expandRowArraw" class="lite-ident" :style="`padding-left:${space + 20}px;`"></span>
<div ref="icon" class="el-table__expand-icon lite-expand-icon" v-if="hasExpandedRow" @click="expand">
<i class="el-icon-arrow-right"></i>
</div>
<div v-if="$slots.default">
<slot :row="row" :index="index" />
</div>
<div v-else>
{{data}}
</div>
</div>
</td>
</template>
</template>
<script>
import {getDeepObjectValue} from '../utils';
export default {
name: 'LiteTableColumn',
props: {
label:{
type: String,
default: '',
},
prop:{
type: String,
default: '',
},
width:{
type: Number || String,
default: null,
},
minWidth:{
type: Number || String,
default: null,
},
align:{
type: String,
default: 'center',
},
formatter:{
type: Function,
default: null,
},
indent:{
type: Number,
default: 16,
},
expandRowArraw:{
type: Boolean,
default: false,
},
sortable:{
type: Boolean,
default: false,
},
headerAlign:{
type: String,
default: '',
}
},
data(){
return{
columnType: '',
index: 0,
row: {},
}
},
mounted() {
this.columnType = this.$parent.columnType;
this.row = this.$parent.row;
this.index = this.$parent.index;
this.$nextTick(()=>{
if(this.$parent.isExpand && this.$refs.icon){
this.$refs.icon.style.transform = 'rotate(90deg)';
}
else if(this.$refs.icon){
this.$refs.icon.style.transform = 'rotate(0deg)';
}
})
},
methods: {
expand(){
this.$parent.expand();
},
SetAscending(){
this.$parent.sort(this.prop, 'ascending');
//this.order = this.order == "ascending" ? null : "ascending";
},
SetDescending(){
this.$parent.sort(this.prop, 'descending');
//this.order = this.order == "descending" ? null : "descending";
},
setOrder(){
if(this.$parent.sortStatus.prop == this.prop){
if(this.$parent.sortStatus.order == 'ascending'){
this.$parent.sort(this.prop, 'descending');
}else if(this.$parent.sortStatus.order == 'descending'){
this.$parent.sort(this.prop, null);
}else{
this.$parent.sort(this.prop, 'ascending');
}
}else{
this.$parent.sort(this.prop, 'ascending');
}
},
},
computed: {
data(){
if(this.formatter){
let cellValue = getDeepObjectValue(this.row, this.prop);
return this.formatter(this.row, cellValue, this.index);
}
return getDeepObjectValue(this.row, this.prop);
},
Style(){
let ret = {};
if(this.minWidth){
ret.minWidth = typeof this.minWidth == "number" ? (this.minWidth + 'px') : this.minWidth;
}
// if(!this.minWidth && this.width){
// ret.width = typeof this.width == "number" ? (this.width + 'px') : this.width;
// }
ret.textAlign = this.align;
return ret;
},
thClass(){
if(this.headerAlign){
switch(this.headerAlign){
case 'left':
return 'lite-td--left';
case 'right':
return 'lite-td--right';
default:
return 'lite-td--center';
}
}else{
return this.tdClass;
}
},
tdClass(){
switch(this.align){
case 'left':
return 'lite-td--left';
case 'right':
return 'lite-td--right';
default:
return 'lite-td--center';
}
},
hasExpandedRow(){
return this.expandRowArraw && this.row.children && this.row.children.length > 0;
},
space(){
let totalSpace = this.$parent.space * this.indent;
return totalSpace;
},
asc(){
if(this.$parent.sortStatus.prop == this.prop && this.$parent.sortStatus.order == "ascending"){
return "color: #409EFF;";
}
return "";
},
des(){
if(this.$parent.sortStatus.prop == this.prop && this.$parent.sortStatus.order == "descending"){
return "color: #409EFF;";
}
return "";
},
},
watch: {
'$parent.isExpand': function(val){
if(val && this.$refs.icon){
this.$refs.icon.style.transform = 'rotate(90deg)';
}else if(this.$refs.icon){
this.$refs.icon.style.transform = 'rotate(0deg)';
}
}
}
}
</script>
<style>
.lite-td{
display: flex;
align-items: center;
flex-direction: row;
}
.lite-td--left{
justify-content: flex-start;
}
.lite-td--center{
justify-content: center;
}
.lite-td--right{
justify-content: flex-end;
}
.lite-expand-icon{
transition: 'all 0.3s',
}
.lite-caret-wrapper{
margin-left: 5px;
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.lite-ascending{
top: 3px;
position: relative;
}
.lite-descending{
top: -3px;
position: relative;
}
.lite-sort-th{
cursor: pointer;
}
th{
color: var(--el-table-header-font-color);
}
.lite-ident{
display: inline-block;
width: 0;
}
</style>
该组件功能不再进行具体赘述,主要是增加了排序的表头样式,排序的功能函数,以及展开所需的样式。
总结
本次主要新增了展开行与表格搜索功能。简单增加了一些样式信息。以及其他一些简单的样式信息。(主要原因是完成目标功能只需要这些功能,element-plus的表格功能实在太卡了。)