vue 表格树 固定表头

参考网上黄龙的表格树进行完善,并添加固定表头等的功能,目前是在iview的项目中实现,如果想在element中实现的话修改对应的元素标签及相关写法即可。

 

<!--
   @events  @on-row-click 单击行或者单击操作按钮方法
            @on-selection-change  多选模式下 选中项变化时触发
            @on-sort-change  排序时有效,当点击排序时触发
   @props   data 显示的结构化数据
            columns 表格列的配置描述 sortable:true 开启排序功能
            showHeader 是否显示表头
            type: 'selection'为多选功能 type: 'template' 为操作功能 slot为插槽名
 -->
<template>
<div ref="table" class='autoTable tree-grid'>
  <!-- <div ref="table" :style="{width:treeGridWidth}" class='autoTable tree-grid'> -->
  <div ref="header" class="tree-grid-header" v-if="showHeader">
    <table class="table table-bordered hl-tree-table">
      <colgroup>
        <col v-for="(column, index) in cloneColumns" :width="column.width" :align="column.align" :key="index">
        <col v-if="showVerticalScrollBar" :width="scrollBarWidth"/>
      </colgroup>
      <thead>
        <tr>
          <th v-for="(column,index) in cloneColumns" :key="column.key">
            <div v-if="column.type === 'selection'" class="selection-wrapper">
              <Checkbox v-model="checks" @click="handleCheckAll"></Checkbox>
              <input type="checkbox" v-model="checks" @click="handleCheckAll" class="selection-checkbox">
            </div>
            <div v-else class="tree-grid-cell">{{renderHeader(column, index)}}
              <span class="ivu-table-sort" v-if="column.sortable">
                <Icon type="md-arrow-dropup" :class="{on: column._sortType === 'asc'}" @click.native="handleSort(index, 'asc')" />
                <Icon type="md-arrow-dropdown" :class="{on: column._sortType === 'desc'}" @click.native="handleSort(index, 'desc')" />
              </span>
            </div>
          </th>
          <th v-if="showVerticalScrollBar" rowspan="1"></th>
        </tr>
      </thead>
    </table>
  </div>
  <div ref="body" class="tree-grid-body" :style="{height:tBodyHeight}">
    <table ref="bodyTable" class="table table-bordered hl-tree-table">
      <colgroup ref="bodyColgroup">
        <col v-for="(column, index) in cloneColumns" :width="column.width" :align="column.align" :key="index">
      </colgroup>
      <tbody>
        <tr v-for="(item,index) in initGridData" :key="item.id" v-show="show(item)" :class="{'child-tr':item.parent}">
          <td v-for="(column,snum) in columns" :key="column.key">
            <div v-if="column.type === 'selection'" class="selection-wrapper">
              <CheckboxGroup v-model="checkGroup">
                <Checkbox :label="item.id"><span></span></Checkbox>
              </CheckboxGroup>
              <input type="checkbox" :value="item.id" v-model="checkGroup" @click="handleCheckClick(item,$event,index)" class="selection-checkbox">
            </div>
            <div v-if="column.type === 'template'" class="tree-grid-cell">
              <slot :name="column.slot" :scope="item"></slot>
            </div>
            <div @click="toggle(index,item)" v-if="!column.type" class="tree-grid-cell">
              <template v-if='snum===iconRow()'>
                <i v-html='item.spaceHtml'></i>
                <span v-if="item.children&&item.children.length>0" class="tree-grid-arrow" :class="{'tree-grid-arrow-open': item.expanded}" >
                  <i class="ivu-icon ivu-icon-ios-arrow-forward"></i>
                </span>
                <i v-else class="ms-tree-space"></i>
              </template>{{renderBody(item,column)}}
            </div>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
  </div>
</template>
<script>
let cached
export default {
  name: 'treeGrid',
  props: {
    columns: {
      type: Array,
      default: () => []
    },
    data: {
      type: Array,
      default: () => []
    },
    showHeader: {
      type: Boolean,
      default: true
    },
    height: {
      type: [Number, String]
    }
  },
  data () {
    return {
      initGridData: [], // 处理后数据数组
      cloneColumns: [], // 处理后的表头数据
      showVerticalScrollBar: false,
      scrollBarWidth: undefined,
      checkGroup: [], // 复选框数组
      checks: false, // 全选
      screenWidth: document.body.clientWidth, // 自适应宽
      headerHeight: 0,
      columnsWidth: {},
      timer: false, // 控制监听时长
      dataLength: 0 // 树形数据长度
    }
  },
  computed: {
    treeGridWidth () {
      let treeGridWidth = this.$el ? this.$el.offsetWidth : '1000'
      return treeGridWidth
    },
    tBodyHeight () {
      return parseFloat(this.height) > 0 ? (parseFloat(this.height) - parseFloat(this.headerHeight)) + 'px' : 'auto'
    }
  },
  watch: {
    screenWidth (val) {
      if (!this.timer) {
        this.screenWidth = val
        this.timer = true
        setTimeout(() => {
          this.timer = false
        }, 400)
      }
    },
    data () {
      if (this.data) {
        this.dataLength = this.Length(this.data)
        this.initData(this.deepCopy(this.data), 1, null)
        this.checkGroup = this.renderCheck(this.data)
        if (this.checkGroup.length === this.dataLength) {
          this.checks = true
        } else {
          this.checks = false
        }
        this.showScrollBar()
      }
    },
    columns: {
      handler () {
        this.cloneColumns = this.makeColumns()
      },
      deep: true
    },
    checkGroup (data) {
      this.checkAllGroupChange(data)
    }
  },
  mounted () {
    if (this.data) {
      this.dataLength = this.Length(this.data)
      this.initData(this.deepCopy(this.data), 1, null)
      this.cloneColumns = this.makeColumns()
      this.checkGroup = this.renderCheck(this.data)
      if (this.checkGroup.length === this.dataLength) {
        this.checks = true
      } else {
        this.checks = false
      }
    }
    // 绑定onresize事件 监听屏幕变化设置宽
    this.$nextTick(() => {
      this.screenWidth = document.body.clientWidth
      this.headerHeight = this.showHeader ? this.$refs.header.clientHeight : 0
      this.cloneColumns = this.makeColumns()
      this.showScrollBar()
    })
    window.onresize = () => {
      return (() => {
        window.screenWidth = document.body.clientWidth
        window.screenHeight = document.body.clientHeight
        this.screenWidth = window.screenWidth
        this.headerHeight = this.showHeader ? this.$refs.header.clientHeight : 0
        this.cloneColumns = this.makeColumns()
        this.showScrollBar()
      })()
    }
  },
  methods: {
    showScrollBar () {
      this.$nextTick(() => {
        // console.error(this.$refs.bodyTable.clientHeight, this.$refs.body.clientHeight)
        if (this.$refs.bodyTable.clientHeight > this.$refs.body.clientHeight) {
          if (!this.showVerticalScrollBar) {
            this.showVerticalScrollBar = true
            this.scrollBarWidth = this.getScrollBarSize()
          }
        } else {
          if (this.showVerticalScrollBar) {
            this.showVerticalScrollBar = false
          }
        }
      })
    },
    // 有无多选框折叠位置优化
    iconRow () {
      for (let i = 0, len = this.columns.length; i < len; i++) {
        if (this.columns[i].type === 'selection') {
          return 1
        }
      }
      return 0
    },
    // 排序事件
    handleSort (index, type) {
      this.cloneColumns.forEach(col => {
        col._sortType = 'normal'
      })
      if (this.cloneColumns[index]._sortType === type) {
        this.cloneColumns[index]._sortType = 'normal'
      } else {
        this.cloneColumns[index]._sortType = type
      }
      this.$emit('on-sort-change', this.cloneColumns[index]['key'], this.cloneColumns[index]['_sortType'])
    },
    // 点击某一行事件
    RowClick (data, event, index, text) {
      let result = this.makeData(data)
      this.$emit('on-row-click', result, event, index, text)
    },
    // 点击事件 返回数据处理
    makeData (data) {
      const t = this.type(data)
      let o
      if (t === 'array') {
        o = []
      } else if (t === 'object') {
        o = {}
      } else {
        return data
      }

      if (t === 'array') {
        for (let i = 0; i < data.length; i++) {
          o.push(this.makeData(data[i]))
        }
      } else if (t === 'object') {
        for (let i in data) {
          if (i !== 'spaceHtml' && i !== 'parent' && i !== 'level' && i !== 'expanded' && i !== 'isShow' && i !== 'load') {
            o[i] = this.makeData(data[i])
          }
        }
      }
      return o
    },
    // 处理表头数据
    makeColumns () {
      let columns = this.deepCopy(this.columns)
      let tableWidth = this.$el.offsetWidth
      let noWidthLength = 0
      let nanWidthLength = 0
      let widthSum = 0
      columns.forEach((column, index) => {
        column._index = index
        column._sortType = 'normal'
        if (column.width) {
          if (!/^(-?\d+)(\.\d+)?$/.test(column.width)) {
            let width = column.width
            if (width.slice(-1) === '%') {
              let percent = (column.width).slice(0, -1)
              column.width = ''
              column._width = ''
              nanWidthLength += 1
              column._nanWidth = percent
            } else {
              this.$Message.error('请输入正确的宽度:数字(例如:100)或者百分比(例如:10%)')
            }
          } else {
            widthSum += column.width
            column._width = column.width
          }
        } else {
          noWidthLength += 1
          column._width = ''
        }
        column._width = column.width ? column.width : ''
      })
      if (nanWidthLength > 0) {
        columns.forEach((column, index) => {
          if (column._nanWidth) {
            column.width = parseInt((tableWidth - widthSum) * column._nanWidth / 100)
            // column.width = (tableWidth - widthSum) * column._nanWidth / 100
            column._width = column.width
            widthSum += column.width
          }
        })
      }
      if (noWidthLength > 0) {
        columns.forEach((column, index) => {
          if (column._width === '') {
            column.width = parseInt((tableWidth - widthSum) / noWidthLength)
            // column.width = (tableWidth - widthSum) / noWidthLength
            column._width = column.width
          }
        })
      }
      return columns
    },
    // 数据处理 增加自定义属性监听
    initData (data, level, parent) {
      this.initGridData = []
      let spaceHtml = ''
      for (let i = 1; i < level; i++) {
        spaceHtml += '<i class="ms-tree-space"></i>'
      }
      data.forEach((item, index) => {
        item = Object.assign({}, item, {
          'parent': parent,
          'level': level,
          'spaceHtml': spaceHtml
        })
        if ((typeof item.expanded) === 'undefined') {
          item = Object.assign({}, item, {
            'expanded': false
          })
        }
        if ((typeof item.show) === 'undefined') {
          item = Object.assign({}, item, {
            'isShow': false
          })
        }
        if ((typeof item.isChecked) === 'undefined') {
          item = Object.assign({}, item, {
            'isChecked': false
          })
        }
        item = Object.assign({}, item, {
          'load': (item.expanded ? 1 : false)
        })
        this.initGridData.push(item)
        if (item.children && item.expanded) {
          this.initData(item.children, level + 1, item)
        }
      })
    },
    // 隐藏显示
    show (item) {
      return ((item.level === 1) || (item.parent && item.parent.expanded && item.isShow))
    },
    toggle (index, item) {
      let level = item.level + 1
      let spaceHtml = ''
      for (let i = 1; i < level; i++) {
        spaceHtml += '<i class="ms-tree-space"></i>'
      }
      if (item.children) {
        if (item.expanded) {
          item.expanded = !item.expanded
          this.close(index, item)
        } else {
          item.expanded = !item.expanded
          if (item.load) {
            this.open(index, item)
          } else {
            item.load = true
            item.children.forEach((child, childIndex) => {
              this.initGridData.splice((index + childIndex + 1), 0, child)
              // 设置监听属性
              this.$set(this.initGridData[index + childIndex + 1], 'parent', item)
              this.$set(this.initGridData[index + childIndex + 1], 'level', level)
              this.$set(this.initGridData[index + childIndex + 1], 'spaceHtml', spaceHtml)
              this.$set(this.initGridData[index + childIndex + 1], 'isShow', true)
              this.$set(this.initGridData[index + childIndex + 1], 'expanded', false)
            })
          }
        }
      }
      this.showScrollBar()
    },
    open (index, item) {
      if (item.children) {
        item.children.forEach((child, childIndex) => {
          child.isShow = true
          if (child.children && child.expanded) {
            this.open(index + childIndex + 1, child)
          }
        })
      }
    },
    close (index, item) {
      if (item.children) {
        item.children.forEach((child, childIndex) => {
          child.isShow = false
          child.expanded = false
          if (child.children) {
            this.close(index + childIndex + 1, child)
          }
        })
      }
    },
    // 点击check勾选框, 父子不相关联
    handleCheckClick (data, event, index) {
      data.isChecked = !data.isChecked
      if (data.isChecked) {
        this.checkGroup.push(data.id)
      } else {
        for (let i = 0; i < this.checkGroup.length; i++) {
          if (this.checkGroup[i] === data.id) {
            this.checkGroup.splice(i, 1)
          }
        }
      }
      this.checkGroup = this.getArray(this.checkGroup)
      let itemsIds = this.getArray(this.checkGroup.concat(this.All(this.data)))
      if (this.checkGroup.length === itemsIds.length) {
        this.checks = true
      } else {
        this.checks = false
      }
    },
    // checkbox 全选 选择事件
    handleCheckAll () {
      this.checks = !this.checks
      if (this.checks) {
        this.checkGroup = this.getArray(this.checkGroup.concat(this.All(this.data)))
      } else {
        this.checkGroup = []
      }
      // this.$emit('on-selection-change', this.checkGroup)
    },
    // 数组去重
    getArray (a) {
      let hash = {}
      let len = a.length
      let result = []
      for (let i = 0; i < len; i++) {
        if (!hash[a[i]]) {
          hash[a[i]] = true
          result.push(a[i])
        }
      }
      return result
    },
    checkAllGroupChange (data) {
      if (this.dataLength > 0 && data.length === this.dataLength) {
        this.checks = true
      } else {
        this.checks = false
      }
      this.$emit('on-selection-change', this.checkGroup)
    },
    All (data) {
      let arr = []
      data.forEach((item) => {
        arr.push(item.id)
        if (item.children && item.children.length > 0) {
          arr = arr.concat(this.All(item.children))
        }
      })
      return arr
    },
    // 返回树形数据长度
    Length (data) {
      let length = data.length
      data.forEach((child) => {
        if (child.children) {
          length += this.Length(child.children)
        }
      })
      return length
    },
    // 返回表头
    renderHeader (column, $index) {
      if ('renderHeader' in this.columns[$index]) {
        return this.columns[$index].renderHeader(column, $index)
      } else {
        return column.title || '#'
      }
    },
    // 返回内容
    renderBody (row, column, index) {
      return row[column.key]
    },
    // 默认选中
    renderCheck (data) {
      let arr = []
      data.forEach((item) => {
        if (item._checked) {
          arr.push(item.id)
        }
        if (item.children && item.children.length > 0) {
          arr = arr.concat(this.renderCheck(item.children))
        }
      })
      return arr
    },
    // 深度拷贝函数
    deepCopy (data) {
      let t = this.type(data)
      let o
      let i
      let ni
      if (t === 'array') {
        o = []
      } else if (t === 'object') {
        o = {}
      } else {
        return data
      }
      if (t === 'array') {
        for (i = 0, ni = data.length; i < ni; i++) {
          o.push(this.deepCopy(data[i]))
        }
        return o
      } else if (t === 'object') {
        for (i in data) {
          o[i] = this.deepCopy(data[i])
        }
        return o
      }
    },
    type (obj) {
      let toString = Object.prototype.toString
      let map = {
        '[object Boolean]': 'boolean',
        '[object Number]': 'number',
        '[object String]': 'string',
        '[object Function]': 'function',
        '[object Array]': 'array',
        '[object Date]': 'date',
        '[object RegExp]': 'regExp',
        '[object Undefined]': 'undefined',
        '[object Null]': 'null',
        '[object Object]': 'object'
      }
      return map[toString.call(obj)]
    },
    getScrollBarSize (fresh) {
      if (this.$isServer) return 0
      if (fresh || cached === undefined) {
        const inner = document.createElement('div')
        inner.style.width = '100%'
        inner.style.height = '200px'
        const outer = document.createElement('div')
        const outerStyle = outer.style
        outerStyle.position = 'absolute'
        outerStyle.top = 0
        outerStyle.left = 0
        outerStyle.pointerEvents = 'none'
        outerStyle.visibility = 'hidden'
        outerStyle.width = '200px'
        outerStyle.height = '150px'
        outerStyle.overflow = 'hidden'
        outer.appendChild(inner)
        document.body.appendChild(outer)
        const widthContained = inner.offsetWidth
        outer.style.overflow = 'scroll'
        let widthScroll = inner.offsetWidth
        if (widthContained === widthScroll) {
          widthScroll = outer.clientWidth
        }
        document.body.removeChild(outer)
        cached = widthContained - widthScroll
      }
      return cached
    }
  },
  beforeDestroy () {
    window.onresize = null
  }
}
</script>
<style lang="less">
.tree-grid {
  @keyframes opacityChild{
    0% { opacity: 0; }
    50% { opacity: .5; }
    100% { opacity: 1; }
  }
  width: 100%;
  color: #1f2d3d;
  color: #495167;
  &.autoTable {
    overflow: auto;
  }
  .tree-grid-body {
    overflow: auto;
  }
  table {
    width: 100%;
    border-spacing: 0;
    border-collapse: collapse;
    &.hl-tree-table {
      &>tbody{
        &>tr {
          height: 50px;
          background-color: #fff;
          border-bottom: 1px solid #e8eaec;
          &:hover {
            background-color: #ebf7ff;
          }
        }
        &>.child-tr {
          background-color: #fff;
        }
      }
      th>label {
        display: inline-block;
        margin: 0 12px;
      }
    }
    .ivu-icon {
      font-size: 18px;
    }
    .tree-grid-arrow {
      cursor: pointer;
      width: 14px;
      text-align: center;
      display: inline-block;
      i {
        position: relative;
        top: -1px;
        transition: all .2s ease-in-out;
        font-size:14px;
        vertical-align:middle;
      }
      &-open {
        i {
          transform:rotate(90deg);
        }
      }
    }
  }
  .table>tbody>tr>td,
  .table>tbody>tr>th,
  .table>thead>tr>td,
  .table>thead>tr>th {
    vertical-align: middle;
    box-sizing: border-box;
    &:last-child {
      border-right: 0;
    }
  }
  // .table>tbody>tr>td,
  // .table>tbody>tr>th {
  //   border-right: 1px solid #ccc;
  // }
  .table>thead>tr>th {
    text-align: left;
  }
  .table-bordered>thead>tr>td,
  .table-bordered>thead>tr>th {
    height: 32px;
    padding: 0;
    vertical-align: middle;
    background: #E1E4E5;
    border-right: 1px solid #fff;
    .tree-grid-cell {
      padding: 0 12px;
    }
  }
  .tree-grid-cell {
    padding: 0 12px;
    font-size: 12px;
  }
  .ms-tree-space {
    position: relative;
    top: 1px;
    display: inline-block;
    font-style: normal;
    font-weight: 400;
    line-height: 1em;
    width: 14px;
    height: 14px;
  }
  .ms-tree-space::before {
    content: "";
  }
  .selection-wrapper {
    position: relative;
    text-align: center;
    width: 18px;
    height: 18px;
    margin: 0 auto;
    vertical-align: middle;
    .selection-checkbox {
      position: absolute;
      left: 0;
      top: 0;
      z-index: 2;
      width: 18px;
      height: 18px;
      vertical-align: middle;
      opacity: 0;
      cursor: pointer;
    }
    .ivu-checkbox-wrapper {
      position: absolute;
      left: 0;
      top: 0;
      z-index: 1;
      margin: 0;
      line-height: 15px;
    }
  }
}
</style>

 

posted @ 2018-12-07 18:11  知九  阅读(2234)  评论(0编辑  收藏  举报