vue 仿el-table原理实现表格

element-ui是国内最流行的的vue开源框架,el-table组件在element-ui整个框架中是最复杂、最重要的部分。其中涉及到的知识有JSX,render渲染函数,组件间的状态管理等等。出于好奇和挑战,在网上受教于el的源代码以及网上相关内容资料。完成了一个简单基础的table组件 就叫 sd-table吧。

支持功能: 自定义列、支持自定义插槽、支持自定义排序、支持全选、多选、分页回调、宽高样式等。

el-table 源码传送门:element: Element 是一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,提供了配套设计资源,帮助你的网站快速成型。由饿了么公司前端团队开源。 - Gitee.com

1.效果及使用方法

1.1 使用方法:

使用方法如下,看看是不是特别熟悉?与 el-table是否类似呢?

<sd-table :data = "tableData"
            ref="sdTable"
            :width="'800'"
            :height="'500'"
            :total="1000"
            :current-page="currentPage"
            @selection-change="handleSelectionChange"
            @loadMore = 'loadMore'
  >
    <sd-table-column  type="selection" width="20">
    </sd-table-column>
    <sd-table-column  width="100" label="solt列">
        <template slot-scope="scope">
          <img src="../assets/vip-icon-hui-5.png"  style="width: 25px">
          <span>{{ scope.row.name }}</span>
        </template>
    </sd-table-column>
    <sd-table-column prop="name" label="姓名" width="200" sortable></sd-table-column>
    <sd-table-column prop="date" label="日期" width="200" sortable></sd-table-column>
    <sd-table-column  prop="address" label="地址" ></sd-table-column>
  </sd-table>

        1.2效果图

2.代码

2.1  sd-table-body.js

        sd-table-body 主要是渲染整体表格,使用render 函数 循环二维数组 遍历tr行 遍历 td列。

export default {
  name:'sd-table-body',
  computed:{
    table(){
      return this.$parent
    }
  },
  props:{
    store: {
      require: true
    }
  },
  methods:{
    getBodyCellStyle(column){
      return {
        width: column.width + 'px',
        'text-align': column.align
      }
    },
  },
  render(h) {
    let tableData = this.table.tableData ;
    let columns = this.store.getColumns() ;
    let tableWidth = this.store.getTableWidth() ;
    return (
      <table cellspacing="0" border='0' cellpadding="0" style={{width: tableWidth + 'px'}}>
        {tableData.map((row, index) =>
          <tr  class={['sd-table-tr',row.$checked?'sd-table-tr-choose':'']}>
            {columns.map(column => <td class={'sd-table-body__td'} style={this.getBodyCellStyle(column)}>
              <div>{column.renderCell(row, index)}</div>
            </td>)}
          </tr>)
        }
      </table>
    )
  }
}

2.2  sd-table-column.js

        sd-table-column.js 此组件主要是渲染 列相关内容,判断type 是selection或 index 或者是自定义插槽。若type=“selection” 为多选框,type=“index” 为 行序号 自定义插槽则直接显示自定义插槽内容,插槽内容可以为组件或html。

代码如下:

let columnIdSeed = 0 ;
export default  {
  name: 'sd-table-column',
  computed:{
    tableColumnId(){
      return 'columnId-' + (columnIdSeed++)
    },
    table(){
      return this.$parent
    },
  },
    props:{
      label: String,
      prop: String,
      width: String,
      align: String,
      type: String,
      sortable:{
        type:Boolean,
        default:false,
      }
    },
  data(){
    return{
      column:{},
      value_2: false,
    }
  },
    methods:{
      renderCell(data,index){
        let curIndex = index + 1 ;
        if(this.type ==='index') {
          return curIndex ;
        }
        if(this.type ==='selection') {
          this.column.renderCell = (data,index) => (
            <div class="cell">
            <input type="checkbox"   checked={data.$checked}  on-click={ ($event) => this.selectionChange($event, data) }  id={index}/>
          </div>
          );
          return ;
        }
        if(this.prop) return data[this.prop]
        return this.$scopedSlots.default({
          $index: curIndex,
          row: data
        })
      },
      selectionChange(event,row){
        let isExist = false ;

        if(row.$checked){
          //row.$checked = !row.$checked
          this.$set(row,'$checked',!row.$checked);
        }else{
          this.$set(row,'$checked',true);
          //row.$checked = true ;
        }

        if(row.$checked){
          this.table.selectRowData.forEach((item)=>{
            if(item == row){
              isExist = true ;
            }
          })
          if(!isExist){
            this.table.selectRowData.push(row) ;
          }
        }else{
          for(let i = 0 ; i <  this.table.selectRowData.length ; i ++){
            if(this.table.selectRowData[i] == row){
              this.table.selectRowData.splice(i,1);
            }
          }
        }
        this.table.$emit("selection-change",this.table.selectRowData) ;
      },
      test(event,data){
        console.log(event.target.id);
      }
    },
    watch:{
      value_2(newVal){
        console.log(newVal)
      }
    },
    created(){
      this.column = {
        tableId: this.table.tableId,
        columnId: this.tableColumnId,
        label: this.label,
        prop: this.prop,
        originWidth: this.width,
        width: 0 ,
        slots: this.$slots.default,
        align: this.align,
        renderCell: this.renderCell,
        type: this.type,
        sortable:this.sortable,
      }
      this.table.store.insertColumn(this.column)
    },
    render(h){
      return h('div', this.$slots.default) ;
    }
}

2.3  sd-table-head.js

此组件是表头内容,组件拿到columns 这个表头数组,通过render函数内遍历可得到表头行,这里涉及到一个chekbox排序。 不涉及复杂表头,复杂表头以后研究。

export default {
  name: 'sd-table-head',
  data(){
    return{
      columns:[],
      // 实现排序功能
      "asc-n": (a, b) => a - b,
      "desc-n": (a, b) => b - a,
      "asc-s": (a, b) => a.localeCompare(b),
      "desc-s": (a, b) => b.localeCompare(a),
    }
  },
  computed:{
    table(){
      return this.$parent
    }
  },
  props:{
    store:{
      require: true
    }
  },
  methods:{
    getHeaderCellStyle(column){
      return {
        width: column.width + 'px'
      }
    },
    handleSortClick(event, column, givenOrder) {
      event.stopPropagation();
      if (!column.sortable) return;
      this.$set(this.table,'order',givenOrder);
      console.log(this.table.order)
      this.sort(givenOrder,column) ;
      this.$emit('sort-change',givenOrder);
  },
    sort(type,column) {
      // 是否排序
      if (column.sortable) {
        // 对父父亲元素进行排序
        this.$parent.tableData.sort((a, b) => {
          // 调用排序规则,传入排序方式asc、desc,然后判断数据类型进行拼接后调用方法实现排序
          return this[type + this.getSortType(a[column.prop])](
            a[column.prop],
            b[column.prop]
          );
        });
      }
    },
    selectAll(){
      this.table.isSelectedAll = !this.table.isSelectedAll;
      if(this.table.isSelectedAll ){
        this.table.toggleRowSelection(this.table.tableData) ;
      }else{
        this.table.clearSelection();
      }
    },
    // 处理排序类型
    getSortType(val) {
      // 返回标识串进行拼接
      return typeof val === "string" ? "-s" : "-n";
    },
  },
  created(){
    this.columns = this.store.getColumns() ;
  },
  render(h) {
    let columns = this.store.getColumns() ;
    let tableWidth = this.store.getTableWidth() ;
    return (
      <table cellspacing="0" border="0" cellpadding="0" style={{width: tableWidth+'px'}}>
            {

              <tr>{columns.map(column=>
                  <td  style={this.getHeaderCellStyle(column)}>
                    {
                      (column.type =='selection')?
                        (<input  type='checkbox' checked={this.table.isSelectedAll} on-click={ ($event) => this.selectAll() }/>):
                        (column.label)
                    }
                    {
                      column.sortable ? (<span
                        class="caret-wrapper">
                          <i class={['sort-caret ascending',this.table.order=='asc'?'ascending ':'']}
                             on-click={ ($event) => this.handleSortClick($event, column, 'asc') }>
                          </i>
                          <i class={['sort-caret descending',this.table.order=='desc'?'descending':'']}
                             on-click={ ($event) => this.handleSortClick($event, column, 'desc') }>
                          </i>
                        </span>) : ''
                    }
                  </td>
              )}
            </tr>
            }

      </table>
    );
  },
}

2.4  sd-table-store.js

store 主要是保存列数据以及表的基本数据信息,方便以上各个组件初始化使用

export default class SdTableStore {
  constructor(tableId) {
    this.storeid = 'store-' + tableId ;
    this.columns = [] ;
    this.columnLabelMap = {} ;
    this.realTableWidth = 0 ;

  }
  insertColumn (column){
    this.columns.push(column) ;
    this.columnLabelMap[column.columnId] = column.label
  }
  getColumns(){
    return this.columns ;
  }
  updateTableWidth (width) {
    this.realTableWidth = width ;
  }
  getTableWidth () {
    return this.realTableWidth ;
  }
}

2.5 util.js

此工具方法为了计算列宽。


function  calcColumnWidth (columns, table) {
  let bodyWidth = table.$el.clientWidth - 24
  let tableWidth = 0
  let flexColumns = []
  flexColumns = columns.filter(column => {
    if (typeof column.originWidth !== 'string') return column
  })
  for (let column of columns) {
    column.width = column.originWidth || 80
    tableWidth += parseInt(column.width)
  }
  // 宽度有富余
  if (tableWidth < bodyWidth) {
    // 富余宽度
    let flexWidth = bodyWidth - tableWidth
    let flexSpaceWidth = parseInt(flexWidth / flexColumns.length)
    flexColumns[0].width += flexWidth - flexSpaceWidth * (flexColumns.length - 1)
    for (let i = 1; i < flexColumns.length; i++) {
      flexColumns[i].width += flexSpaceWidth
    }
    table.store.updateTableWidth(bodyWidth)
  } else {
    table.store.updateTableWidth(tableWidth)
  }
  return columns ;
}
export {calcColumnWidth}

2.6 sd-table.vue

此组件是以上组件的父组件,负责组织和调用以上组件。

代码如下:

<template>

  <div class="sd-table"
       :class="[{
            'sd-table--border': border
        }]"
       :style="tableStyle">
    <!-- 隐藏列 -->
    <div class="hidden-columns"><slot></slot></div>
    <div class="pos-relative">
      <div class="header-warp" :ref="headRef">
        <sd-table-head class="sd-table-head" :store="store"></sd-table-head>
      </div>
      <!-- 遮挡竖直滚动条 -->
      <div class="hiddenBlock"></div>
    </div>
<!--    <div v-loading="loadingFlag" class="pos-relative" :class="textAlign">-->
    <div  class="pos-relative" :class="textAlign">
      <div :style="bodyWrapperStyle" class="body-wrap" :ref="bodyRef" @scroll="handleScroll">
        <sd-table-body :store="store"></sd-table-body>
      </div>
      <div class="noData flex-midcenter" v-show="!data.length">暂无数据</div>
    </div>
  </div>
</template>

<script>
import SdTableStore from "./sd-table-store";
import SdTableHead  from "./sd-table-head";
import SdTableBody  from "./sd-table-body";
import {calcColumnWidth} from '@/utils/util';
let tableIdSeed = 0 ;
export default {
  name: "sd-table",
  data () {
    return {
      store: null,
      curPage: 1,
      allPages: 0,
      bodyWrapperStyle: {
        height: this.height+'px',
        'text-align': this.align,
        overflow: 'auto'
      },
      loadingFlag: true,
      tableStyle: {
        width: this.width
      },
      fixedHeight: 1,
      tableData: [],
      selectRowData:[],
      isSelectedAll:false,
      order:'asc',
    }
  },
  computed: {
    tableId () {
      return 'tableId-' + (tableIdSeed++)
    },
    bodyRef () {
      return 'tableBody-' + tableIdSeed
    },
    tBody () {
      return this.$refs[this.bodyRef]
    },
    headRef () {
      return 'tableHead-' + tableIdSeed
    },
    tHead () {
      return this.$refs[this.headRef]
    },
    'textAlign' () {
      return 'align-' + this.align
    }
  },
  components:{
    SdTableHead,
    SdTableBody,
  },
  props: {
    data: {
      type: Array,
      default () {
        return []
      }
    },
    width: {
      type: String,
      default: '100%'
    },
    total: {
      type: [Number, String],
      default: 0
    },
    align: {
      type: String,
      default: 'center'
    },
    height: {
      type: String,
      default: '300'
    },
    border: Boolean,
    currentPage: {
      type: Number,
      default: 1
    }
  },
  methods: {
    initData () {
      this.curPage = 1
      this.tableData = []
      this.allPages = Math.ceil(parseInt(this.total) / this.data.length)
      //this.tBody.scrollTop = 0
    },
    handleScroll (e) {

      this.tHead.scrollLeft = this.tBody.scrollLeft
      // fixedHeight修正由水平滚动条带来的高度计算误差
      if ((this.tBody.scrollTop + this.tBody.clientHeight + this.fixedHeight >= this.tBody.scrollHeight) && (this.curPage < this.allPages)) {
        this.loadingFlag = true
        this.$emit('loadMore', ++this.curPage)
      }
    },
    toggleRowSelection(rows){
      // 交集
      this.tableData.forEach((item)=>{
        rows.forEach((itm)=>{
          if(item == itm){
            this.$set(item,"$checked",true);
          }
        })
      })
      this.selectRowData = rows ;
      console.log(this.selectRowData) ;
    },
    clearSelection(){
      this.selectRowData = [] ;
      this.isSelectedAll = false;
      this.tableData.forEach((item)=>{
        this.$set(item,"$checked",false);
      })
    },
    doLayout () {
      calcColumnWidth(this.store.columns, this)
    }
  },
  watch: {
    data: {
      deep: true,
      immediate: true,
      handler (n, o) {
        this.loadingFlag = false
        if (this.currentPage === 1) {
          this.initData()
        }
        this.tableData = [...this.tableData, ...n]
      }
    }
  },
  created () {
    let store = new SdTableStore(this.tableId)
    this.store = store;
  },
  mounted () {
    if (!this.data.length) {
      this.loadingFlag = false
    }

    let self = this;
    self.doLayout()
    window.addEventListener('resize', function () {
      self.doLayout()
    })
  },
}
</script>

<style>

.sd-table{
  width: 100%;
  padding: 5px 20px 5px 20px;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
}
.sd-table .header-warp{
  width: 100%;
  display: flex;
  flex-direction: row;
  align-items: center;
  font-size: 12px;
  padding: 10px 0 10px 0;
}

.body-wrap {
  position: relative;
}


.sd-table-tr{
  height: 60px;
  background-color: white;
  border-top: 1px solid #eeeeee;
  display: flex;
  align-items: center;
  font-size: 14px;
  transition: border-color .3s,background-color .3s,color .3s;
  flex-shrink: 0;
}


.sd-table-tr:hover{
  padding: 0 0 0 0;
  background-color: #e8f8f7;
}

/**当选中,item背景状态**/
.sd-table-tr-choose{
  padding: 0 0 0 0;
  background-color: #e8f8f7 !important;
}

.checkboxshow{
  visibility:visible !important;
}

.sd-table .item input[type=checkbox]{
  visibility:visible;
}

.sd-table .item:hover .share{
  display: block;
}


.sd-table .item:hover .itesd-time>span{
  display: none;
}


.hiddenBlock {
  position: absolute;
  background: $head-bg;
  width: 25px;
  height: calc(100% - 1px);
  top: 0;
  right: 0;
  border-bottom: $border;
}


.forbidden-child-pointer-events >* {
  pointer-events: none;
}




.sd-table .caret-wrapper {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  height: 34px;
  width: 24px;
  vertical-align: middle;
  overflow: initial;
  position: relative;
}
.sort-caret.ascending {
  border-bottom-color: #c0c4cc;
  top: 5px;
}
.sort-caret-ascending {
  border-bottom-color: #409eff;
}

.sort-caret.descending {
  border-top-color: #409eff;
}
.sort-caret.descending {
  border-top-color: #c0c4cc;
  bottom: 7px;
}

 .sort-caret {
  width: 0;
  height: 0;
  border: 5px solid transparent;
  position: absolute;
  left: 7px;
}


</style>

2.6 test-table.vue

test-table.vue 用于调用测试表格使用。mbutton 为我自定义的组件,请自行去掉或替换。

我们可以看到基本与el-table 的使用用法一致。当然目前功能不完善,功能性不如element-ui。但是自定义插槽、多选排序以及分页功能是我们经常用到的,足够用了。

<template>
<div>
  <sd-table :data = "tableData"
            ref="sdTable"
            :width="'800'"
            :height="'500'"
            :total="1000"
            :current-page="currentPage"
            @selection-change="handleSelectionChange"
            @loadMore = 'loadMore'
  >
    <sd-table-column  type="selection" width="20">
    </sd-table-column>
    <sd-table-column  width="100" label="solt列">
        <template slot-scope="scope">
          <img src="../assets/vip-icon-hui-5.png"  style="width: 25px">
          <span>{{ scope.row.name }}</span>
        </template>
    </sd-table-column>
    <sd-table-column prop="name" label="姓名" width="200" sortable></sd-table-column>
    <sd-table-column prop="date" label="日期" width="200" sortable></sd-table-column>
    <sd-table-column  prop="address" label="地址" ></sd-table-column>
  </sd-table>
  <button style="width: 100px;height: 50px" @click="add">添加数据</button>
  <button style="width: 100px;height: 50px" @click="figureChoose">指定选择</button>
  <button style="width: 100px;height: 50px" @click="cancel">取消选择</button>
</div>
</template>

<script>
import SdTable from "../components/table/sd-table";
import SdTableColumn from "../components/table/sd-table-column";
import Mbutton from "../components/mbutton";
export default {
  name: "test-table",
  components: {Mbutton, SdTableColumn, SdTable},
  data(){
    return {
    tableData: [
      {
        no: 0,
        date: "2022-01-01",
        name: "徐凤年",
        address: "北凉国",
      },
      {
        no: 1,
        date: "2022-01-02",
        name: "姜泥",
        address: "楚国",
      },
      {
        no: 2,
        date: "2022-01-03",
        name: "王仙芝",
        address: "武帝城",
      },
      {
        no: 3,
        date: "2022-01-04",
        name: "李淳罡",
        address: "剑仙一键开天门",
      }
      ]
      ,
      currentPage: 1,
    }
  },methods:{
    add(){
      let one = {
        no: 5,
        date: "2022-01-05",
        name: "呵呵姑娘",
        address: "不详",
      };
      this.tableData.push(one);
    },
    figureChoose(){
      this.$refs.sdTable.toggleRowSelection([this.tableData[1],this.tableData[2]])
    },
    handleSelectionChange(val){
      console.log("handleSelectionChange",val);
    },
    cancel(){
      this.$refs.sdTable.clearSelection();
      console.log("cancel");
    },loadMore(val){
      console.log(val);
      this.currentPage = val ;
    }
  }
}
</script>

<style scoped>

</style>

感谢阅读!

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Vue.js 中,可以使用 Element UI 中的 el-table 表格组件实现表格排序。el-table 组件提供了一个 sortable 属性,可以设置表格的排序方式。 以下是一个简单的 el-table 表格排序实现示例: HTML代码: ``` <template> <div> <el-table :data="tableData" :sortable="true"> <el-table-column prop="name" label="姓名" sortable></el-table-column> <el-table-column prop="age" label="年龄" sortable></el-table-column> <el-table-column prop="gender" label="性别" sortable></el-table-column> </el-table> </div> </template> ``` JavaScript代码: ``` <script> export default { data() { return { tableData: [ { name: '张三', age: 20, gender: '男' }, { name: '李四', age: 22, gender: '男' }, { name: '王五', age: 18, gender: '女' } ] } } } </script> ``` 在 el-table 组件中,设置 sortable 属性为 true,然后在每一个 el-table-column 列中设置 sortable 属性即可完成表格排序的功能。在 el-table-column 中设置 prop 属性为表格数据源中的对应属性名,设置 label 属性为列名。 如果需要对表格数据进行默认排序,可以在 el-table 组件中设置 sort-by 和 sort-direction 属性。sort-by 属性为表格数据源中的对应属性名,sort-direction 属性为排序方向,值为 ascending(升序)或 descending(降序)。 ``` <el-table :data="tableData" :sortable="true" :sort-by="'age'" sort-direction="ascending"> ``` 需要注意的是,el-table 的数据源必须是一个数组,每个元素都是一个对象,对象的属性名对应每一列的 prop 属性值。 这样,就可以通过 el-table 表格组件快速方便地实现表格排序功能了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值