element-ui对el-table组件进行二次封装,并进行功能扩展

扩展功能:

1. 实现使用Ctrl/Shift+鼠标左键不连续/连续选择,并支持批量勾选(具体看handleRowClick)

Ctrl+鼠标左键(不连续选择):

  • 使用变量缓存当前选中行集合,el-table绑定row-class-name函数,判断目标行是否被选中,选中则添加自定义类名
  • 监听row-click事件,事件触发时判断Ctrl键是否按下,若按下,则将此行添加到选中行集合

Shift+鼠标左键(连续选择):

  • 思路同上,过判断的keyCode不一样
  • row-click事件中需要分别计算当前行和点击行的序号,根据序号将区间内的所有行数据取出并缓存。

批量勾选:

  • 监听select事件,判断是否勾选,并对所有选中行调用toggleRowSelection方法

效果:

2. 实现正选反选当前行(具体看handleRowClick)

监听row-click,判断点击行是否为当前行,若是调用setCurrentRow(),反之调用setCurrentRow(row)

效果:
element-ui对el-table组件进行二次封装,并进行功能扩展_第1张图片
PS:

  • 选中是选中,勾选是勾选,不要混淆概念了,勾选是el-table内部实现的
  • 要实现以上两个功能,一定不要再去监听current-change事件了,需要自己对当前行进行处理,按照el-table的逻辑,current-change事件永远发生在row-click事件之前,所以你在row-click获取到的当前行永远都是点击行

3. 将数据导出为excel时,使用el-table的列标题作为导出后的列标题(具体看getColumnLabelMap)

思路:获取el-table实例的columns属性,里面包含了每一列的列标题(label)
效果:
element-ui对el-table组件进行二次封装,并进行功能扩展_第2张图片
element-ui对el-table组件进行二次封装,并进行功能扩展_第3张图片
element-ui对el-table组件进行二次封装,并进行功能扩展_第4张图片
element-ui对el-table组件进行二次封装,并进行功能扩展_第5张图片

4. 对row-click进行延时消抖,即触发row-dblclick不再会触发row-click事件(外部监听_row-click,具体看rowClickDeb变量)

效果:
element-ui对el-table组件进行二次封装,并进行功能扩展_第6张图片

5. 根据当前页码、页大小,显示行号

效果:
element-ui对el-table组件进行二次封装,并进行功能扩展_第7张图片
element-ui对el-table组件进行二次封装,并进行功能扩展_第8张图片

实现代码

<template>
  <el-table class="uiot-table"
            :class="{'has-rowcheckbox':showRowCheckbox}"
            ref="table"
            v-loading="loading"
            :row-key="rowKey"
            :border="border"
            :default-sort="defaultSort"
            :highlight-current-row="highlightCurrentRow"
            :data="tableRows"
            :header-row-style="customHeadRowStyle"
            :header-cell-style="customHeadCellStyle"
            :cell-style="customCellStyle"
            :row-style="customRowStyle"
            :height="height"
            :max-height="maxHeight"
            :cell-class-name="customCellClassName"
            :row-class-name="customRowClassName"
            :current-row-key.sync="currentRowKey"
            v-bind="$attrs"
            v-on="$listeners"
            @sort-change="handleSortChange"
            @header-click="handleHeaderClick"
            @row-click="handleRowClick"
            @row-dblclick="handleRowDBlClick"
            @current-change="handleCurrentRowChange"
            @select="handleRowCheck"
            @selection-change="handleSelectionChange">
    <template v-if="$scopedSlots.default||(columns&&columns.length>0)">
      <el-table-column v-if="showRowCheckbox"
                       type="selection"
                       width="50px"
                       align="center">
      </el-table-column>
      <el-table-column v-if="showLinenum"
                       type="index"
                       label="No."
                       header-align="center"
                       align="center"
                       :width="$idxNoWidth"
                       :index="getRowIndex">
      </el-table-column>
    </template>
    <!-- 如果有slot则渲染slot -->
    <template v-if="!$scopedSlots.default&&columns">
      <el-table-column v-for="(col,index) of columns"
                       :key="index"
                       v-bind="col" />
    </template>
    <slot v-else></slot>
  </el-table>
</template>

<script>
import { exportTo } from '@/utils/exportTo'

function parseOrder(order) {
  return typeof order === 'boolean' ? (order ? 'ascending' : 'descending') : order
}

const computed = {
  /** @returns {Vue} */
  table() {
    return this.$refs.table
  },
  border() {
    return this.tableStyle == 'normal'
  },
  /** @this {number} */
  outsideHeight() {
    return this.$el.clientHeight
  },
  clearSelection() {
    return this.table?.clearSelection
  },
  toggleRowSelection() {
    return this.table?.toggleRowSelection
  },
  toggleAllSelection() {
    return this.table?.toggleAllSelection
  },
  toggleRowExpansion() {
    return this.table?.toggleRowExpansion
  },
  clearSort() {
    return this.table?.clearSort
  },
  clearFilter() {
    return this.table?.clearFilter
  },
  doLayout() {
    return this.table?.doLayout
  },
  store() {
    return this.table?.store
  },
  // el-table内部的currentRow
  _currentRow() {
    return this.store?.states.currentRow
  },
  /** @returns {any[]} */
  _rowSelection() {
    return this.store?.states.selection
  },
}

export default {
  inheritAttrs: false,
  name: 'UiotTable',
  props: {
    /** 是否显示loading */
    loading: {
      type: Boolean,
      default: false,
    },
    /** 自定义列,优先级小于slot.default */
    columns: Array,
    /** 数据源 */
    tableRows: {
      type: Array,
      default: () => []
    },
    /** 表格排序参数 */
    tableSort: Object,
    /** 行数据主键属性 */
    rowKey: {
      type: String,
      default: 'id'
    },
    /** 当前页码 */
    pageIndex: Number,
    /** 页大小 */
    pageSize: Number,
    /** 当前选中行,支持双向绑定*/
    currentRow: Object,
    /** 是否可以反选当前行,即点击当前行时取消选中状态 */
    reverseCurrentRow: Boolean,
    /** 是否显示行号 */
    showLinenum: {
      type: Boolean,
      default: true,
    },
    /** 是否在每行前面显示复选框 */
    showRowCheckbox: {
      type: Boolean,
      default: false,
    },
    /** 表格风格,用于控制样式 */
    tableStyle: {
      type: String,
      validator: value => ['normal', 'concise'].indexOf(value) > -1
    },
    /** 原生属性 */
    size: {
      type: String,
      default: 'mini',
    },
    /** 原生属性 */
    height: {
      type: [String, Number],
      default: '100%',
    },
    /** 原生属性 */
    maxHeight: {
      type: [String, Number],
      default: '100%',
    },
    /** 原生属性,高亮当前行 */
    highlightCurrentRow: {
      type: Boolean,
      default: false,
    },
    /** 原生属性[只写]  -> ps: 设置此属性只会在外观上改变当前行 */
    currentRowKey: [String, Number],
    /** 当前勾选行集合,支持双向绑定 */
    rowSelection: Array,
    /** 原生属性 */
    cellStyle: Function,
    /** 原生属性 */
    rowStyle: Function,
    /** 原生属性 */
    rowClassName: Function,
    /** 原生属性 */
    cellClassName: Function,
  },
  created() {
    // debounce请自己实现
    this.rowClickDeb = this.$utils.debounce((row, column, event) => {
      this.$emit('_row-click', row, column, event)
    }, 300)
  },
  mounted() {
    if (this.tableSort) {
      this.setDefaultSort(this.tableSort.field || '', this.tableSort.order)
    }
    console.log('uiot-table -> mounted', this.table, this.store)
  },
  data() {
    return {
      rowClickDeb: null,
      defaultSort: {
        prop: '',
        order: 'ascending',
      },
      lastTableRows: null,
      cacheData: {
        // 此变量的作用是在handleRowClick事件后才更新当前行,目前仅供reverseCurrentRow功能使用
        // 因为el-table在rowclick事件触发时,内部先处理了当前行,然后再抛出此事件,这样的话参数中的row永远都是当前行
        currentRow: null,
        lastRow: null,
        pageIndex: 1,
        pageSize: 1
      },
      selectedRows: [],
      selectedRange: [-1, -1],
    }
  },
  computed: computed,
  watch: {
    'tableSort.default'(nval) {
      // 备份当前sort
      const { field, order } = this.tableSort
      // 获取到默认sort
      this.tableSort.default()
      // 设置默认sort
      this.setDefaultSort(this.tableSort.field, this.tableSort.order)
      // 还原sort
      this.tableSort.field = field
      this.tableSort.field = order
    },
    showLinenum(val) {
      if (val) {
        if (typeof this.pageIndex != 'number') throw '如果显示行号,必须传入"pageIndex"属性'
        if (typeof this.pageSize != 'number') throw '如果显示行号,必须传入"pageSize"属性'
      }
    },
    tableRows(nval, oval) {
      // 如果有当前行,则清除
      if (this.cacheData.currentRow) {
        this.setCurrentRow()
      }
      this.$nextTick(() => {
        if (this.doLayout) this.doLayout()

        // 只有数据源改变了才去更新页码和页大小,否则还在loading状态,行号就已经变了,视觉效果不好
        this.cacheData.pageIndex = this.pageIndex || 1
        this.cacheData.pageSize = this.pageSize || 1
      })
    },
    currentRow(nval, oval) {
      if (nval != this.cacheData.currentRow) {
        // 这里一定要nextTick,因为在watch表格数据源变化时,会清除当前行
        this.$nextTick(() => this.setCurrentRow(nval))
      }
    }
  },
  methods: {
    // #region 方法
    scrollTo() {
      this.table?.$refs.bodyWrapper?.scrollTo(...arguments)
    },
    sort(prop, order) {
      return this.table?.sort(prop, parseOrder(order))
    },
    setDefaultSort(prop, order) {
      this.defaultSort.prop = prop ?? ''
      this.defaultSort.order = parseOrder(order)
    },
    /**
     * 重写setCurrentRow方法,请勿调用el-table提供的setCurrentRow
     */
    setCurrentRow(row = null) {
      if (this.cacheData.currentRow != row) {

        this.cacheData.lastRow = this.cacheData.currentRow
        this.cacheData.currentRow = row

        if (this.currentRow != this.cacheData.currentRow) {
          this.$emit('update:currentRow', this.cacheData.currentRow)
        }
        this.$emit('_current-change', this.cacheData.currentRow, this.cacheData.lastRow)

        console.log('表格当前行改变 -> ', 'currentRow', this.cacheData.currentRow, 'lastRow', this.cacheData.lastRow)
      }

      if (this._currentRow != row) {
        // 如果el-table内部的当前行不是传入的row,则更新一下
        this.table.setCurrentRow(row)
      }
    },
    // #endregion

    // #region 事件处理
    handleRowCheck(selection, row) {
      if (this.showRowCheckbox && this.selectedRows.length > 0 && this.selectedRows.includes(row)) {
        const check = selection.includes(row)
        for (const row of this.selectedRows) {
          this.toggleRowSelection(row, check)
        }
      }
    },
    /**
     * @param {MouseEvent} event
     */
    handleRowClick(row, column, event) {
      event.preventDefault()

      console.log('row-click -> ', row, column, event)
      this.rowClickDeb(row, column, event)

      if (this.showRowCheckbox) {
        if (!this.tableRows && this.selectedRows.length > 0) {
          this.selectedRows = []
          return
        }

        if (event.shiftKey && this.cacheData.currentRow) {
          // 找出上一次点击行与当前点击行的序号
          this.selectedRows = []
          window.getSelection().removeAllRanges()
          let startNo = this.tableRows.findIndex((item) =>
            this.cacheData.currentRow && item === this.cacheData.currentRow
          )
          let endNo = this.tableRows.findIndex((item) => item === row)

          if (startNo != -1 && endNo != -1) {
            // 正序一下
            if (startNo > endNo) {
              let temp = endNo
              endNo = startNo
              startNo = temp
            }
            for (let i = startNo; i <= endNo; i++) {
              this.selectedRows.push(this.tableRows[i])
            }
            console.log('多选', startNo, endNo, this.selectedRows)
            // 多选不需要设置当前行
            return
          }
        }
        else if (event.ctrlKey) {
          if (this.selectedRows.includes(row)) {
            this.selectedRows.$remove(row)
          }
          else {
            this.selectedRows.push(row)
          }
          this.table.setCurrentRow()
          return
          // if()
          // this.toggleRowSelection(row)
        }
      }

      if (this.selectedRows.length > 0) this.selectedRows = []
      if (this.reverseCurrentRow && row === this.cacheData.currentRow) {
        this.setCurrentRow()
      }
      else {
        this.setCurrentRow(row)
      }

    },
    handleRowDBlClick() {
      this.rowClickDeb.cancel()
      console.log('row-dblclick -> ')
    },
    handleHeaderClick() {
    },
    handleCurrentRowChange(newRow, oldRow) {
      // this.cacheData.lastRow = oldRow
      // if (this.currentRow != newRow) {
      //   this.$emit('update:currentRow', newRow)
      // }
      // console.log('表格当前行改变 -> ', newRow, oldRow)
    },
    handleSortChange({ column, prop, order }) {
      console.log(column, prop, order)
      if (typeof this.tableSort == 'object') {
        if (order) {
          this.tableSort.field = prop
          this.tableSort.order = order === 'descending' ? false : true
        }
        else if (typeof this.tableSort.default == 'function') {
          this.tableSort.default()
        }
        else {
          this.tableSort.field = this.defaultSort.prop
          this.tableSort.order = this.defaultSort.order === 'descending' ? false : true
        }
      }
      // this.$emit('sort-change', ...arguments)
    },
    handleSelectionChange(val) {
      if (typeof this.rowSelection != 'object') throw '如果开启了"showRowCheckbox", 必须传入"rowSelection"'
      this.$emit('update:rowSelection', val)
    },
    // #endregion

    getRowIndex(index) {
      return index + 1 + (this.cacheData.pageIndex - 1) * this.cacheData.pageSize //this.loading ? index + 1 + (this.lastPageIndex - 1) * this.lastPageSize : index + 1 + (this.pageIndex - 1) * this.pageSize
    },
    /**
     * 获取table的列头文本(获取到的是i18n翻译后的)
     */
    getColumnLabelMap() {
      const columnLabelMap = {}
      for (const col of this.table.columns) {
        if ((!this.showLinenum && col.type == 'index') || col.type === 'selection') continue
        if (col.property && col.label) {
          columnLabelMap[col.property] = col.label
        }
      }
      return columnLabelMap
    },
    exportTo({ showLinenum = false, columnMap, name }) {
      if (this.tableRows && this.tableRows.length > 0 && this.table.columns.length > 0) {

        columnMap = columnMap ? columnMap : this.getColumnLabelMap()

        // 替换列标题
        const data = this.tableRows.map((row) => {
          const temp = {}
          for (const key in columnMap) {
            // label : value
            temp[columnMap[key]] = row[key]
          }
          return temp
        })
        exportTo({
          data: data,
          dataType: 'json',
          fileName: name
        })
      }
    },

    // #region 样式、类重载
    customCellClassName({ row, column, rowIndex, columnIndex }) {
      let clsname = ''
      if (this.cellClassName) clsname += this.cellClassName(arguments[0]) || ''
      return clsname.trim()

    },
    customRowClassName({ row, rowIndex }) {
      let clsname = ''
      if (this.showRowCheckbox) {
        // if (this._rowSelection && this._rowSelection.indexOf(row) != -1) {
        //   clsname += ' is-checked'
        // }
        // if (rowIndex >= this.selectedRange[0] && rowIndex <= this.selectedRange[1]) {
        // 如果是选中行,添加css类
        if (this.selectedRows.includes(row)) {
          clsname += ' is-selected'
        }
      }
      if (this.rowClassName) clsname += this.rowClassName(arguments[0]) || ''
      return clsname.trim()
    },
    // #region 表格样式
    customHeadCellStyle({ row, column, rowIndex, columnIndex }) {
      switch (this.tableStyle) {
        case 'concise':
          return 'padding:0px;height:30px;'
        case 'normal':
        default:
          return {
            background: '#f0f0f0',
            color: '#505050',
            padding: 0,
            height: '34px'
          }
      }
    },
    customHeadRowStyle({ row, rowIndex }) {
      switch (this.tableStyle) {
        case 'concise':
        case 'normal':
        default:
          return ''
      }
    },
    customRowStyle({ row, rowIndex }) {
      switch (this.tableStyle) {
        case 'concise':
          return ''
        case 'normal':
        default:
          return ''
      }
    },
    customCellStyle({ row, column, rowIndex, columnIndex }) {
      switch (this.tableStyle) {
        case 'concise':
          return { padding: '2px 0 2px 0' }
        case 'normal':
        default:
          return { padding: '3px 0 3px 0' }
      }
    },
    // #endregion 
  }
}
</script>

<style lang='scss'>
.uiot-table {
}
</style>

你可能感兴趣的:(#,element-ui,#,Vue,vue)