后台管理系统element-admin——table组件二次封装

Start

项目中使用表格灰常的频繁👿,所以一个简洁的表格组件,既能让代码变得优雅还可以省去很多重复的操作。

element封装调用

element-ui已经为我们封装好了一层,这是element-ui的写法:

<template>
  <el-table
    :data="tableData"
    style="width: 500px">
    <el-table-column prop="name" label="姓名" width="180"></el-table-column>
    <el-table-column label="日期" width="120">
      <template slot-scope="scope">{{ '加个前缀😝' + scope.row.date }}</template>
    </el-table-column>
    <el-table-column fixed="right" label="操作" width="120">
      <template slot-scope="scope">
        <el-button @click.native.prevent="deleteRow(scope.$index, tableData)" type="text" size="small">
          移除
        </el-button>
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
  export default {
    data() {
      return {
        tableData: [{
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄'
        },{
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀区金沙江路 1518 弄'
        }]
      }
    },
    methods: {
      deleteRow(index, rows) {
        rows.splice(index, 1)
      }
    }
  }
</script>
element-table

虽然已经封装的很好了,但感觉还是不够简洁,并且还想将分页功能、loading功能、拖动功能都通过配置参数的方式来选择是否调用,所以进行了二次封装,看一下封装过后的调用代码:

二次封装后调用

<template>
  <s-table ref="sTable" :data="loadData" :columns="columns">
    <div slot="action" slot-scope="{row}">
      <el-button type="primary" @click="deleteRow(row)">移除</el-button>
    </div>
  </s-table>
<template>

<script>
import STable from '@/components/table/index'
export default {
  components: {
    STable
  },
  data () {
    return {
      columns: [
        {name: '姓名', desc: 'name', width: '100'},
        {name: '日期', desc: 'date', width: '220', filter: true, filterFun: (row) => { return `加个前缀😝 ${row.date}` }},
        {name: '操作', desc: 'action', width: '320', slot: true, fixed: 'right'}
      ],
    }
  },
  methods: {
    loadData: param => {
      return getData()
        .then(res => {
          return res
        })
    }
  }
}
</script>
s-table

引入组件之后,table的数据由组件内部主动调用并返回,表格的每一列不再是 el-table-column 标签,而是由 columns 数组传递每一列信息,还包括了 loading、pagination 组件的逻辑,不用每次都处理同样的逻辑了。其中还包含了拖拽排序、选中操作等常用的功能。

开始写

下面看一下二次封装table组件的思路吧~

JSX写法

JSX 就是Javascript和XML结合的一种格式。React发明了JSX,利用HTML语法来创建虚拟DOM。当遇到 < ,JSX就当HTML解析,遇到 { 就当JavaScript解析。
我为什么用jsx呢?一方面是之前的一版template写法写的实在是看着很"chou",另一方面想学习jsx语法和render函数官方文档看这里

主思路

从结构上来看,主要分为两部分:tablepagination ,而 table 中又有 column

其次就是数据的加载和过滤,默认主动调用数据加载,统一处理好数据结构(table 所需的 list ,分页组件所需的数据等)后,按需返回。

拖动功能的初始化和处理。

table数据的更新机制。


所以主结构是这样的:

render () {
  return (
    <div>
      { this.renderTable() }
      { this.renderPage() }
    </div>
  )
}

因为分页组件根据外部props属性决定使用或者不使用,并且有一些自定义样式,所以改成这样:

render () {
  return (
    <div>
      { this.renderTable() }
      <div style={this.pageClass}>{ this.showPagination ? this.renderPage() : '' }</div>
    </div>
  )
}

renderTable

下面看一下 renderTable 中是怎么写的:

/**
  * renderTable
  */
renderTable () {
  const props = {
    border: true, // table的边框
    size: this.tableSize, // table的大小
    data: this.dataList,  // table需要的数据
  }
  return (
    <el-table ref="multipleTable"
      {...{ props }}  // 属性绑定
      v-loading={this.tableLoading} // table的loading
      on-selection-change={this.changeFun}  // 选中某行的回调
      on-select-all={this.selectAll}> // 选中所有行的回调
      {this.columns.map(item => this.renderTableColumn(item))} // 遍历每一列
    </el-table>
  )
},

renderTable主要思路就是:先配好 table 的属性 props ,return 里返回 table 的结构,结构里 绑定属性和方法 。里面的每一列用 mpa 函数遍历,再用 renderTableColumn 渲染。

renderTableColumn

渲染列的render函数:

/**
  * renderColumn
  */
renderTableColumn (item) {
  // TableColumn配置的属性
  const props = {
    prop: item.desc,
    label: item.name,
    align: 'center',
    key: item.desc,
    width: item.fixedWidth == undefined ? 'auto' : item.fixedWidth,
    minWidth: item.width == undefined ? 'auto' : item.width,
    fixed: item.fixed,
    showOverflowTooltip: item.tooltip ? true : false,
    sortable: item.sortable ? 'custom' : false
  }
  return (
    // 这里需要判断当前列(item.type)的属性,如果有type(通常type用于带select选项的列)就展示带type的列。如果没有就展示正常列。
    item.type
      ? <el-table-column {...{ props }} type={item.type} selectable={item.selectable}/> // 带type的列
      : <el-table-column {...{ props }} {...{scopedSlots: { // 不带type的列
        default: (scope) => {
          if (item.slot) { 
            // 如果设置了slot,就设置一个插槽并且自带返回值
            return this.$scopedSlots[item.desc](scope)
          } else if (item.pic) {
            // 如果设置了pic,就返回一个img的结构
            return (<img src={scope.row[item.desc]} style={{width: item.width - 24 + 'px', borderRadius: '5px'}}/>)
          } else if (item.click) {
            // 如果带有点击事件的,就设置点击后的回调,还带有filter的回调。
            return (<div style={item.style} onClick={this.btnClickfunc.bind(item, scope.row, item.desc)}>{item.filter ? item.filterFun(scope.row) : scope.row[item.desc]}</div>)
          } else {
            // 除去以上几种,普通的都直接返回通用的dom结构,带tooltip、filter的回调。
            return (<div style={item.tooltip ? {...this.tooltip, ...item.style} : item.style}>{item.filter ? item.filterFun(scope.row) : scope.row[item.desc]}</div>)
          }
        }
      }}}/>
  )
},

renderTableColumn主要思路就是:首先每一列的信息进来先看有没有带type,常见的就是带选择框的(也就是type="selection")的,如果是就展示带type的列,如果不是就正常列。

正常列里面如果是带有插槽属性的,就设置一个插槽并传递参数(插槽相关知识可以参考这篇);
如果设置了本列需要展示pic,就返回一个img结构;
如果设置带有点击事件,那就返回一个带有点击回调的结构;
其余的都返回正常结构,并带有tooltip、filter的回调。

renderPage

渲染分页组件的render函数:

/**
  * renderPage
  */
renderPage () {
  const props = {
    background: true,
    small: this.pageSmall,
    currentPage: this.currentPage,
    pageSizes: [15, 30, 60, 120],
    pageSize: this.pageSize,
    total: this.total,
    layout: 'total, sizes, prev, pager, next, jumper'
  }
  return (
    <el-pagination {...{ props }} on-current-change={this.handleCurrentChange} on-size-change={this.handleSizeChange} style={this.pageStyle}/>
  )
},

同样的思路:设置props,绑定属性和方法。

数据加载

结构大概搭好之后考虑一下数据问题,默认是主动加载数据的,但也有不需要一加载组件就请求数据的,所以我们在mounted函数内以 isInit 属性按需请求,下面是 loadData 函数:

/**
  * 数据加载
  */
loadData () {
  this.tableLoading = true  // table的loading
  const param = this.showPagination // 分页信息
    ? { page: this.currentPage, size: this.pageSize }
    : ''
  if (this.data && this.data instanceof Array) { // 如果数据是数组就直接赋值给dataList
    this.tableLoading = false
    this.dataList = this.data
    // 调用初始化拖动方法
    this.dragInit()
  } else {
     // 如果data是一个返回值是promise的函数,就调用并then。
    const result = this.data(param)
    if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
      result.then(rsp => {
        // 处理数据,给table组件、给分页组件
        this.currentPage = rsp.data.currentPage
        this.total = rsp.data.totalElement
        this.tableLoading = false
        this.dataList = rsp.data.list
        // 如果父组件需要返回值就发送过去
        if (this.needData) {
          this.$emit('allBackData', rsp.data)
          this.$emit('backData', rsp.data.list, this.currentPage, this.pageSize)
        }
        // 调用初始化拖动方法
        this.dragInit()
      })
    }
  }
},

采用组件内调用获取数据方法是因为,基本在和后端小伙伴定义数据结构的时候就统一了 table 和 pagination 的结构,所以在组件内部统一过滤数据可以让调用者代码更简洁,如果调用者需要返回值则可以设置属性。

拖动功能

拖动要引入 import Sortable from 'sortablejs' ,父组件调用时如果设置了 drag 那么组件内将初始化拖动功能。看一下初始化拖动的方法:

/**
  * 设置拖动
  */
setSort () {
  const el = this.$refs.multipleTable.$el.querySelectorAll('.el-table__body-wrapper > table > tbody')[0]
  if (this.drag && !this.sortable) {  // 如果没有创建sortable则创建
    this.sortable = Sortable.create(el, {
      ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
      setData (dataTransfer) {
        dataTransfer.setData('Text', '')
      },
      onEnd: evt => { // 拖动后
        const tempIndex = this.newList.splice(evt.oldIndex, 1)[0] // 获取当前拖动选项的id
        this.newList.splice(evt.newIndex, 0, tempIndex) // 在新列表里的新位置插入拖动选项的id
        this.$emit('newSort', this.newList)  // 发送给父组件新列表

        // 拖动后需同步table内data数据
        this.dataList.splice(evt.newIndex, 0, this.dataList.splice(evt.oldIndex, 1)[0]) // 在tableList的新位置插入拖动选项的数据
        let newArray = this.dataList.slice(0)
        this.dataList = []
        this.$nextTick(() => {
          this.dataList = newArray  // 重新赋值给dataList
        })
      }
    })
  } else if (!this.drag && this.sortable) { // 如果不需要拖动 但是创建过sortable 那就销毁
    this.sortable.destroy()
    this.sortable = null
  }
},

拖动后首先获取到当前拖动项的id,放到列表的新位置上,发送给父组件。
之后table的数据也需要同步顺序,把当前拖动项的数据放到新的位置上返回一个新的数组,等到dom结构更新完成之后,重新赋值给dataList,这样就可以保证看到列表的顺序和实际的顺序是一样的。

数据更新

有一些业务场景需要更新table的数据,所以组件里写了一个 refresh 的方法,方便父组件更新数据。

/**
  * 表格重新加载方法
  * 如果参数为 true, 则强制刷新到第一页
  * @param Boolean bool
  */
refresh (bool = false) {
  if (bool) {
    this.currentPage = 1
  }
  this.loadData()
},

总结

整体思路大概就是这样,其中的一些逻辑也是根据后来业务需求加上的,还有一些分页、框选等相关方法这里没过多解释,完整代码戳这里~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354