用vue写轮子的一些心得(九)——table表单组件

 

需求分析

  • 支持斑马纹,默认斑马纹样式;
  • 支持表格边框线,默认没有边框线;
  • 支持table内容padding间距配置(是否为紧凑型);
  • 支持给table设置高度;
  • 支持全选与全选取消,默认不展示;
  • 支持给任意选项排序;
  • 支持请求数据时,展示loading状态;
  • 支持可展开,当表格内容较多不能一次性完全展示时;
  • 支持在table最后一列传自定义内容,比如按钮;
  • 默认固定表头;

 

方法实现

一、table组件传参定义:

一共可传12个参数和1个事件:

  • striped:是否展示斑马纹样式,默认展示;
  • bordered:是否有表格边框线,默认无边框线;
  • compact:table内容间距配置,两种方案:4px和8px,默认8px;
  • height:table容器高度,默认无高度;
  • checkable:是否展示全选项,默认不展示;
  • orderBy:排序方式,比如升序、降序;
  • loading:是否为loading加载状态;
  • extendField:可展开内容数据名;
  • selectedItems:被选中的行,值为数组;
  • colums:表头<th>内容配置,必填;
  • dataSource:表主体<td>内容配置,必填;
  • @update:orderBy:点击排序按钮的事件,可向接口按指定排序方式请求数据;
<t-table :columns="columns"
         :data-source="dataSource"
         bordered-items.sync="selected"
         :order-by.sync="orderBy"
         :loading="loading"
         :selectedItems.sync="selected"
         extendField="description"
         :striped="true"
         bordered
         checkable
         compact
         numberVisible
         :height="400"
         @update:orderBy="sort">
    <template slot-scope="row">
        <button @click="look(row.item)">查看</button>
        <button @click="edit(row.item)">编辑</button>
    </template>
</t-table>

table数据: 

data() {
    return {
        currentPage: 1,
        selected: [],
        columns: [
            {text: '姓名', field: 'name', width: 100},
            {text: '分数', field: 'score'},
        ],
        orderBy: {
            score: 'desc'
        },
        loading: false,
        dataSource: [
            {id:1, name:'A-Tione1', score: 99, description: '11111'},
            {id:2, name:'A-Tione2', score: 98, description: '22222'},
            {id:3, name:'A-Tione3', score: 97},
            {id:4, name:'A-Tione4', score: 96},
            {id:5, name:'A-Tione5', score: 90},
            {id:6, name:'A-Tione6', score: 105},
            {id:7, name:'A-Tione7', score: 80},
            {id:8, name:'A-Tione8', score: 150},
            {id:9, name:'A-Tione9', score: 60},
            {id:10, name:'A-Tione10', score: 107},
        ],
    }
},

二、table组件内部实现:

1、支持斑马纹,默认斑马纹样式

在table标签中定义striped类名,根据props传参来确定是否显示类名,具体的样式由css负责,js负责控制class的显影。

<table class="t-table" ref="table" :class="{bordered, compact, striped: striped}"></table>
props: {
    striped: {
        type: Boolean,
        default: true
    },
    bordered: {
        type: Boolean,
        default: false
    },
    compact: {
        type: Boolean,
        default: false
    },

2、支持表格边框线,默认没有边框线

同上

3、支持table内容padding间距配置(是否为紧凑型)

同上

4、支持给table设置高度

首先在table外加一个包裹容器div,将高度设置在div容器上面,再给容器加上overflow:auto,可使容器里面的table滑动展示。

但这里要注意一个点,我们需要做固定表头,固定表头会使高度有问题(具体请看10、默认固定表头实现),所以我们需要用js计算将table的高度减掉一个表头的高度,这样table容器的总高度才会是设置的正确高度。

<div class="t-table-wrapper" ref="wrapper">
    <div :style="{height, overflow:'auto'}" ref="tableWrapper">
        <table class="t-table" ref="table" :class="{bordered, compact, striped: striped}">
        </table>
    </div>
</div>
props: {
    height: {
      type: Number
    },
},
mounted() {
    let table2 = this.$refs.table.cloneNode(false)
    this.table2 = table2
    let tHead = this.$refs.table.children[0]
    let {height} = tHead.getBoundingClientRect()
    this.$refs.tableWrapper.style.height = this.height-height + 'px'
    table2.appendChild(tHead)
    this.$refs.wrapper.appendChild(table2)
},

5、支持全选与全选取消

在th标签中点击为全选或者全不选,在td标签中点击为单个选中或者不选中。

那么我们先看看th标签中的全选逻辑,onChangeAllItems方法,当用户点击全选checked时,我们判断checked是否为true,如果为true则将dataSource全部遍历,组件外接收并更新selectedItems。如果为checked为false,则为空数组。

inSelectedItems方法判断更新后的每一行数据checked为true还是为false。

另外这里还有一个半选的问题,如果数据没有满选,应该展示一个半选样式(如上图),mdn上说这里半选样式只能在js上面添加,因此我们要在watch上监听一下selectedItems,并适时的加入半选样式。

this.$refs.allChecked.indeterminate = false or true
<table class="t-table" ref="table" :class="{bordered, compact, striped: striped}">
    <thead>
    <tr>
        <th v-if="checkable" :style="{width: '50px'}" class="t-table-center">
            <input ref="allChecked" type="checkbox" @change="onChangeAllItems"
                   :checked="areAllItemsSelected"/>
        </th>
    </tr>
    </thead>
    <tbody>
    <template v-for="(item, index) in dataSource">
        <tr :key="item.id">
            <td v-if="checkable" :style="{width: '50px'}" class="t-table-center">
                <input type="checkbox" @change="onChangeItem(item, index, $event)"
                       :checked="inSelectedItems(item)">
            </td>
        </tr>
    </template>
    </tbody>
</table>
watch: {
    selectedItems() {
        if (this.selectedItems.length === this.dataSource.length) {
            this.$refs.allChecked.indeterminate = false
        } else this.$refs.allChecked.indeterminate = this.selectedItems.length !== 0
    }
},
computed: {
    areAllItemsSelected() { //判断所有元素是否被选中
        const a = this.dataSource.map(item => item.id).sort()
        const b = this.selectedItems.map(item => item.id).sort()
        let equal = true
        if (!b.length) {
            return equal = false
        }
        if (a.length !== b.length) {
            return equal
        }
        for (let i = 0; i < a.length; i++) {
            if (a[i] !== b[i]) {
                equal = false
                break
            }
        }
        return equal
    },
},
methods: {
    inSelectedItems(item) {
        return this.selectedItems.filter(value => value.id === item.id).length > 0
    },
    onChangeItem(item, index, e) {
        let selected = e.target.checked
        let copy = JSON.parse(JSON.stringify(this.selectedItems))
        if (selected) {
            copy.push(item)
        } else {
            copy = copy.filter(i => i.id !== item.id)
        }
        this.$emit('update:selectedItems', copy)
    },
    onChangeAllItems(e) {
        let selected = e.target.checked
        this.$emit('update:selectedItems', selected ? this.dataSource : [])
    }
}

6、支持给任意选项排序

确定要排序的字段,score: 'desc' 。然后通过监听事件@update:orderBy="sort"监听触发排序事件,然后向后端请求排序数据。

这里要注意如果外部参数orderBy没有展示排序字段的,则再在table组件中对应列不展示排序样式也不可点击触发。

外部参数设置:

<template>
    <t-table :columns="columns"
             :data-source="dataSource"
             :order-by.sync="orderBy"
             @update:orderBy="sort">
    </t-table>
</template>


data() {
    return {
        columns: [
            {text: '姓名', field: 'name', width: 100},
            {text: '分数', field: 'score'},
        ],
        orderBy: {
            score: 'desc'
        },
        loading: false,
        dataSource: [
            {id:1, name:'A-Tione1', score: 99, description: '11111'},
            {id:2, name:'A-Tione2', score: 98, description: '22222'},
            {id:3, name:'A-Tione3', score: 97},
            {id:4, name:'A-Tione4', score: 96},
            {id:5, name:'A-Tione5', score: 90},
            {id:6, name:'A-Tione6', score: 105},
            {id:7, name:'A-Tione7', score: 80},
            {id:8, name:'A-Tione8', score: 150},
            {id:9, name:'A-Tione9', score: 60},
            {id:10, name:'A-Tione10', score: 107},
        ],
    }
},
methods: {
    sort() { //模拟排序,发送ajax请求,获取新数据
        this.loading = true
        setTimeout(()=> {
            this.dataSource.sort((a, b) => a.score - b.score)
            this.loading = false
        },1000)
    },

},

组件内部 :

<th :style="{width: column.width + 'px'}" v-for="column in columns" :key="column.field">
    <div class="t-table-header">
        {{column.text}}
        <span v-if="column.field in orderBy" class="t-table-sorter"
              @click="changeOrderBy(column.field)">
            <t-icon name="asc" :class="{active: orderBy[column.field] === 'asc'}"></t-icon>
            <t-icon name="desc" :class="{active: orderBy[column.field] === 'desc'}"></t-icon>
        </span>
    </div>
</th>

methods: {
    changeOrderBy (key) {
        const copy = JSON.parse(JSON.stringify(this.orderBy))
        let oldValue = copy[key]
        if (oldValue === 'asc') {
            copy[key] = 'desc'
        } else if (oldValue === 'desc') {
            copy[key] = true
        } else {
            copy[key] = 'asc'
        }
        this.$emit('update:orderBy', copy)
    },
}

7、支持请求数据时,展示loading状态

通常loading状态是用于处理请求接口时等待接口响应的样式展示。

内部实现无需js代码,只需在html中增加v-if判断,但loading为true时展示菊花,否则不展示即可。展示菊花为css样式,将其table组件全部遮盖展示loading动画即可。

外部数据传参:

<t-table :loading="loading"</t-table>

sort() {
    this.loading = true
    setTimeout(()=> {
        this.dataSource.sort((a, b) => a.score - b.score)
        this.loading = false
    },1000)
},

内部实现:

<template>
    <div class="t-table-wrapper" ref="wrapper">
        <div :style="{height, overflow:'auto'}" ref="tableWrapper">
            <table class="t-table" ref="table" :class="{bordered, compact, striped: striped}">
                ...
            </table>
        </div>
        <div v-if="loading" class="t-table-loading">
            <t-icon name="loading"></t-icon>
        </div>
    </div>
</template>

8、支持可展开,当表格内容较多不能一次性完全展示时

在外部传入进来的数据结构中,extendField字段为展开内容字段,当展示时则可点击展开。

这里的展开是通过新开一个tr标签来实现的,要注意的是新的tr标签中的td是需要做合并操作的,这里需要动态计算colspan,这里用到了计算属性expendedCellColSpan方法来计算table中的一行有多少列,以此来计算合并的项数。

展开的内容确定则是通过一个数组来实现的,点击某一行找到这行的id,然后push到数组中,再进行hash匹配。否则splice将这行数据从数组中删除。

<table class="t-table" ref="table" :class="{bordered, compact, striped: striped}">
    <thead>
    <tr>
        <th v-if="extendField" :style="{width: '50px'}" class="t-table-center">展开</th>
    </tr>
    </thead>
    <tbody>
    <template v-for="(item, index) in dataSource">
        <tr :key="item.id">
            <td v-if="extendField" :style="{width: '50px'}" class="t-table-center" @click="expendItem(item.id)">
                <t-icon class="t-table-expendIcon" name="right"></t-icon>
            </td>
        </tr>
        <tr v-if="inExpendedIds(item.id)" :key="`${item.id}-expend`">
            <td :colspan="columns.length + expendedCellColSpan">
                {{item[extendField] || '无'}}
            </td>
        </tr>
    </template>
    </tbody>
</table>
computed: {
    expendedCellColSpan() {
        let result = 0
        if (this.checkable) {result += 1}
        if (this.numberVisible) {result += 1}
        if (this.extendField) {result += 1}
        if (this.$scopedSlots.default) {result += 1}
        return result
    }
},
methods: {
    inExpendedIds(id) {
        return this.expendedIds.indexOf(id) > -1
    },
    expendItem(id) {
        if (this.inExpendedIds(id)) {
            this.expendedIds.splice(this.expendedIds.indexOf(id), 1)
        } else {
            this.expendedIds.push(id)
        }
    },
}

 9、支持在table最后一列传自定义内容,比如按钮

在组件内部的solt 上定义一个属性,名字可以任意取(比如item)。然后我们在组件入参的位置加上一个slot-scope=”xx“,通过xx.item即可拿到solt上传递的值(例如一行数据的值)。从而实现在组件外部进行查看编辑等等逻辑。

组件传参:

<t-table :columns="columns"
         :data-source="dataSource“>
    <template slot-scope="row">
        <button @click="look(row.item)">查看</button>
        <button @click="edit(row.item)">编辑</button>
    </template>
</t-table>

组件内部: 

<table class="t-table" ref="table" :class="{bordered, compact, striped: striped}">
    <thead>
    <tr>
        <th ref="actionsHeader" v-if="$scopedSlots.default">操作</th>
    </tr>
    </thead>
    <tbody>
    <template v-for="(item, index) in dataSource">
        <tr :key="item.id">
            <td v-if="$scopedSlots.default">
                <div ref="actions" style="display: inline-block">
                    <slot :item="item"></slot>
                </div>
            </td>
        </tr>
    </template>
    </tbody>
</table>

 10、默认固定表头

思路:

因为table组件内部的td不能单独设置overflow: auto;,所以固定表头并不能很容易地实现,因此我们要另想办法。

怎么办呢?再创建一个table,将旧table的th装进新table中去,然后再通过css将新table放置在旧table上方即可。因此整个table的高度是由新table+旧table组成的,因此我们在设置容器高度时需要减去多出来的新table高度才正确。

这里我们在mounted钩子中就需要做些dom魔法:临时创建一个table容器, 再将旧table的children的第一个节点也就是th挪到新table中。注意这里虽然th移动了位置,但是th上面的node节点与监听事件仍然与旧table中的th保持一致,并不会丢失。简单来看仅仅是样式移动了位置,逻辑并没有移动。

同理,如果我们要做固定列,比如一行的内容太多,则需要固定编辑的列。我们也可以使用相同思路,重新创建一个新table,再将旧table中的编辑列移至新table中,然后再通过css调整到正确的位置即可。

组件内部实现:

mounted() {
    let table2 = this.$refs.table.cloneNode(false)
    this.table2 = table2
    let tHead = this.$refs.table.children[0]
    let {height} = tHead.getBoundingClientRect()
    this.$refs.tableWrapper.style.height = this.height-height + 'px'
    table2.appendChild(tHead)
    this.$refs.wrapper.appendChild(table2)
}

 

 

完整代码可参考我的GitHub:https://github.com/A-Tione/tione/blob/master/src/table/table.vue

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值