vue制作表格table组件


以前做的组件,留下来欣赏,这里用了scss和阿里矢量图标,可以自己用其他方式替代

<template>
  <div class="FitTable" :style="tableStyle">
    <div class="progress" v-if="loading">
      <ScaleLoader class="progress-spinner" :loading="loading" :color="loadingColor"></ScaleLoader>
    </div>
    <div class="FitTable-header" ref="header" :style="headerStyle">
      <div class="FitTable-header__content">
        <div v-if="ifSelect" class="cell-select" :style="{height: headerHeight + 'px'}">
          <div class="cell-select-icon"></div>
        </div>
        <div v-if="selectData" class="FitTable-cell select-cell" :style="{ headerHeight + 'px'}">
          <div @click.stop="changeHeaderSelectStatus(selectHeaderStatus)" class="select-icon" :class="selectHeaderStatus ? ['icon-aliduihao1', 'selected']:['no-select']"></div>
        </div>
        <div v-for="(item, index) in columns" :key="item.unique || index" class="FitTable-cell"
             :style="{minWidth: typeof item.minWidth === 'number'?item.minWidth + 'px':item.minWidth,
              item.width,
             textAlign: item.textAlign || 'center',
             height: headerHeight + 'px'}"
             :title="item.title">
          <slot :name="item.slotHeader" :data="{...item,index}">
            <div class="header-title">{{item.title}}
              <div v-show="item.sort !== void(0)" class="table-sort">
                <div class="table-sort-asc" :style="{opacity:(item.sort!=='desc')?1:0.3}" @click.stop="changeSort(item,'asc')"></div>
                <div class="table-sort-desc" :style="{opacity:(item.sort!=='asc')?1:0.3}" @click.stop="changeSort(item,'desc')"></div>
              </div>
            </div>
          </slot>
        </div>
      </div>
    </div>
    <div :style="tbodyHeight" ref="tableTbody">
      <FitScroll>
        <div class="FitTable-body" :style="{tableLayout:tableLayout}">
          <NoData :message="message" v-if="!loading&&!data.length"></NoData>
          <div class="FitTable-body-row">
            <template v-for="(item,index) in data">
              <slot :name="`index-header${index}`">
              </slot>
              <div class="FitTable-body-row_line" :key="(dataKey && item[dataKey]) || index" :style="{height: lineHeight}">
                  <div class="cell-select" v-if="ifSelect">
                    <div class="cell-select-icon"></div>
                  </div>
                  <div v-if="selectData" class="FitTable-cell select-cell" :style="{ headerHeight + 'px'}">
                    <div @click.stop="changeSelectStatus(item[selectKey], selectDataSet.has(item[selectKey]))" class="select-icon" :class="selectDataSet.has(item[selectKey]) ? ['icon-aliduihao1', 'selected']:['no-select']"></div>
                  </div>
                  <div v-for="(_item, _index) in columns" :key="_item.unique || _index" class="FitTable-cell"
                       :style="{minWidth: typeof _item.minWidth === 'number'?_item.minWidth + 'px':_item.minWidth,
                        _item.width,
                       textAlign: _item.textAlign || 'center'}"
                        @click.stop="showExpand(index, _item.expand)">
                    <div class="FitTable-cell-content">
                      <div class="FitTable-cell-content-data">
                        <slot :name="_item.slot" :data="{...item,index, data: item}">
                          <span :title="item[_item.key] || _item.defaultVal" style="color: #DBE5E8;" v-if="!_item.expand">
                            {{item[_item.key] || _item.defaultVal}}
                          </span>
                          <span class="expand-icon" :class="expands.includes(index)?'icon-alileft':'icon-alibottom-triangle'" v-else></span>
                        </slot>
                      </div>
                    </div>
                  </div>
              </div>
              <div class="expand" :key="`expand-${(dataKey && item[dataKey]) || index}`" :style="expandStyle" v-show="expands.includes(index)">
                <slot name="expand" :data="{...item,index, data: item}">
                </slot>
              </div>
              <slot :name="`index-footer${index}`"></slot>
            </template>
          </div>
        </div>
      </FitScroll>
    </div>
  </div>
</template>

<script>
 /* eslint-disable */
import ScaleLoader from 'vue-spinner/src/ScaleLoader.vue'
import NoData from '../NoData'
 import resize from '@/mixins/eventResize';
export default {
  name: "FitTable",
  components: {
    ScaleLoader,
    NoData
  },
  props: {
    maxHeight: {
      type: String,
      default: null
    },
    // 表格宽度,默认100%
     {
      type: String,
      default: '100%'
    },
    height: {
      type: Number | String,
      default: '100%'
    },
    ifSelect: {
      type: Boolean,
      default: false
    },
    // 起始索引
    // startIndex:{
    //   type: Number,
    //   default: 0
    // },
    /* TODO columns接受值: 对象数组
        title: 表头名称
        slotHeader: 表头插槽名称
        slot: 表体插槽
        defaultVal: 数据不存在,判断为false时的值,默认值
        unique: 默认索引值,必须唯一,便于vue优化
        key: 对应prop:{data}中数据的键值
        列固定宽度
        minWidth: 对应列最小宽度
        textAlign:文本对齐方式
        expand:展开expand插槽的信息
        onlySort: 设置为true,该字段排序时,其他字段排序重置
        sort: 默认排序方式 空字符为不排序,asc为升序, desc为降序
    */
    columns: {
      type: Array,
      default: () => []
    },
    data: {
      type: Array,
      default: () => []
    },
    // 唯一值,防止不必要的错误发生
    selectKey: {
      type: String,
      default: null
    },
    // 选择框
    selectData: {
      type: Array,
      default: null
    },
    message:{
      type: String,
      default: '查询不到数据'
    },
    headerHeight: {
      type: Number,
      default: 35
    },
    backgroundColor: {
      type: String,
      // default: '#3c6180',
      default: 'rgba(60,97,128,0.3)'
    },
    lineHeight: {
      type: String,
      default: ''
    },
    // prop:{data}中唯一的值,不传则通过索引优化表格
    dataKey: {
      type: String,
      default: null
    },
    loading: {
      type: Boolean,
      default: false
    },
    loadingColor: {
      type: String,
      default: '#45d1ea'
    },
    tableLayout: {
      type: String,
      default: 'fixed'
    }
  },
  mixins: [resize],
  computed: {
    tableStyle: {
      get () {
        return { this.width,maxHeight: this.maxHeight,height:typeof this.height === 'number' ?this.height + 'px':this.height};
      }
    },
    headerStyle () {
      return {this.tbodyWidth + 'px', backgroundColor: this.backgroundColor,tableLayout: this.tableLayout};
    },
    tbodyHeight () {
      const height = this.tableHeight > (this.maxHeight || Infinity)?this.maxHeight:this.tableHeight;
      return { height: height -this.headerHeight + 'px',overflowY: 'auto' };
    },
    expandStyle () {
      return {
         this.tbodyWidth + 'px'
      }
    },
    selectDataSet () {
      return this.selectData && new Set(this.selectData);
    },
    selectHeaderStatus () {
      if (this.selectDataSet && this.selectKey) {
        return this.selectDataSet.size === this.data.length;
      } else {
        return false;
      }
    }
  },
  watch: {
    height () {
      if (String(this.height).indexOf('%') !== -1) {
        this.tableHeight = this.$el.offsetHeight * parseInt(this.height) / 100
      } else {
        this.tableHeight = this.height;
      }
    }
  },
  data: () => ({
    rowWidth: null,
    // 将自定义的列width值收集起来的和
    customWidth: { count: 0, value: 0 },
    tbodyWidth: 0,
    tableHeight: 0,
    theaderHeight: 0,
    expands: []
  }),
  mounted() {
      // const width = this.$el.offsetWidth;
      // this.theaderHeight = this.$refs['header'].offsetHeight;
    this.$nextTick(() => {
      if (String(this.height).indexOf('%') !== -1) {
        this.tableHeight = this.$el.offsetHeight * parseInt(this.height) / 100;
      } else {
        this.tableHeight = this.height;
      }
      const getTbodyWidth = () => {
        requestAnimationFrame(() => {
          if (!this.$refs['tableTbody']) {
            return getTbodyWidth();
          }
          this.tbodyWidth = this.$refs['tableTbody'].clientWidth;
          if (!this.tbodyWidth) {
            getTbodyWidth();
          }
        })
      };
      getTbodyWidth();
    });
      // this.rowWidth = (width - this.customWidth.value) / (this.columns.length - this.customWidth.count) + 'px';
      // // eslint-disable-next-line no-console
      window.addEventListener('resize', this.resize);
      this.$once('hook:beforeDestroy', () => window.removeEventListener('resize', this.resize))
  },
  methods: {
    changeSort (item, type) {
      if (item.sort === 'asc' || item.sort === 'desc') {
        item.sort = ''
      } else {
        if (type === 'asc') {
          item.sort = 'desc'
        } else {
          item.sort = 'asc'
        }
      }
      if (item.onlySort) {
        this.columns.forEach(e => {
          if (e !== item && e.sort !== void (0)) {
            e.sort = ''
          }
        })
      }
      this.$emit('changeSort', item)
    },
    resize(el) {
      // if (this.width.indexOf('%') !== -1) {
      //   const width = this.$el.offsetWidth;
      //   this.rowWidth = (width - this.customWidth.value) / (this.columns.length - this.customWidth.count) + 'px'
      // }
      setTimeout(() => {
        this.theaderHeight = this.$refs['header'].offsetHeight;
        if (String(this.height).indexOf('%') !== -1) {
          // console.log(this.$el.offsetHeight,this,document.getElementsByClassName('FitTable')[0].offsetHeight, 8899)
          this.tableHeight = this.$el.offsetHeight * parseInt(this.height) / 100
        }
        this.tbodyWidth = this.$refs['tableTbody'].clientWidth;
      });
    },
    changeHeaderSelectStatus (status) {
      if (status) {
        this.selectData.splice(0, this.selectData.length);
      } else {
        this.data.forEach(e => {
          if (!this.selectDataSet.has(e[this.selectKey])) {
            this.selectData.push(e[this.selectKey]);
          }
        });
      }
    },
    changeSelectStatus (unique, status) {
      if (status) {
        this.selectData.reverse().forEach((e, index) => {
          if (e === unique) {
            this.selectData.splice(index, 1);
          }
        })
      } else {
        this.selectData.push(unique);
      }
    },
    showExpand (index, expand) {
      if (!expand) {
        return;
      }
      const findIndex = this.expands.findIndex(e => e === index);
      let ifExpand = true;
      if (~findIndex) {
        this.expands.splice(findIndex, 1);
        ifExpand = false;
      } else {
        this.expands.push(index);
      }
      this.$nextTick(() => {
        this.$emit('expand', {index, ifExpand});
      });
    }
  }
}
</script>

<style lang="scss" scoped>
  @import "../../styles/_scroll";
  $sortWidth: 0.5vw;
  $sortColor: #ccc;
  $tableCell: table-cell;
  .FitTable {
    position: relative;
    .select-icon {
      margin: 0 auto;
       15px;
      height: 15px;
      border-radius: 2px;
    }
    .selected {
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 1rem;
      color: white;
      background-color: #3A97E2;
    }
    .no-select {
      border: 1px solid white;
    }
    &-header {
      display: table;
      box-sizing: border-box;
      // border-bottom: 1px solid rgba(255,255,255,0.32);
      &__content {
        display: table-header-group;
        border-bottom: 1px solid rgba(255,255,255,0.3);
        .cell-select {
           35px;
          display: $tableCell;
          position: relative;
          &-icon {
            position: absolute;
            left: 0;
            top: 0;
            right: 0;
            bottom: 0;
            margin: auto;
            height: 15px;
             15px;
            border: 1px solid white;
          }
        }
        .FitTable-cell {
          display: $tableCell;
          vertical-align: middle;
          > div {
            //position: relative;
            //top: 50%;
            //transform: translateY(-50%);
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
          }
          .header-title {
            margin: 1px 5px;
            color: #DBE5E8;
            display: inline-flex;
            align-items: center;
            .table-sort {
              margin-left: 0.2vw;
              display: inline-block;
              &-asc {
                cursor: pointer;
                border-top: $sortWidth solid transparent;
                border-bottom: $sortWidth solid $sortColor;
                border-left: calc(#{$sortWidth} * 2 / 3) solid transparent;
                border-right: calc(#{$sortWidth} * 2 / 3) solid transparent;
                margin-bottom: 0.15vh;
              }
              &-desc {
                cursor: pointer;
                border-top: $sortWidth solid $sortColor;
                border-bottom: $sortWidth solid transparent;
                border-left: calc(#{$sortWidth} * 2 / 3) solid transparent;
                border-right: calc(#{$sortWidth} * 2 / 3) solid transparent;
                margin-top: 0.15vh;
              }
            }
          }
        }
      }
    }
    &-body {
       100%;
      display: table;
      box-sizing: border-box;
      &-row {
        display: table-row-group;
        &_line {
          display: table-row;
          .cell-select {
             35px;
            display: $tableCell;
            position: relative;
            &-icon {
              position: absolute;
              left: 0;
              top: 0;
              right: 0;
              bottom: 0;
              margin: auto;
              height: 15px;
               15px;
              border: 1px solid white;
            }
          }
          &:nth-child(odd) {
            background-color: transparent;
          }
          &:nth-child(even) {
            background-color: rgba(158,158,158,0.1);
          }
          &:hover {
            background-color: rgba(158, 158, 158, 0.1);
          }
          .FitTable-cell {
            display: $tableCell;
            vertical-align: middle;
            &-content {
              height: 30px;
              padding: 1px 5px;
              &-data {
                position:relative;
                top: 15px;
                transform: translateY(-50%);
                overflow: hidden;
                white-space: nowrap;
                text-overflow: ellipsis;
                .expand-icon {
                  color: white;
                }
              }
            }
          }
        }
      }
    }
  }
  .progress {
    position: absolute;
    text-align: center;
     100%;
    height: 100%;
    // z-index: -1;
    &-spinner{
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
  }
</style>




原文地址:https://www.cnblogs.com/smallZoro/p/12722344.html