關於 vue data-grid 思路與解法

andyyu0920
  • 1.7k

小弟試做了 data table 的 component,需求是希望可以具備排序, 搜尋, 過濾, 分頁 的功能下面是程式碼地webpackbin

如果無法連線最下面會附上程式碼:

今天的問題是因為這三個功能需求是累計的所以在做法上我選擇透過 filters

<div class="u-tr" v-for="entry in source | filterBy searchQuery | keep 'length' 'length' | orderBy sortKey sortOrders[sortKey] | limitBy size start">
  <div class="u-td" v-for="column in columns" :style="{ minWidth: column.width, flex: column.width ? 'none' : 1 }">
    <slot :name="entry.id + '_' + column.key">{{entry[column.key]}}</slot>
  </div>
</div>
entry in source | filterBy searchQuery | keep 'length' 'length' | orderBy sortKey sortOrders[sortKey] | limitBy size start

在沒有分頁的時候,vue 官方文件已提供一個優秀精簡的範例, 但為了完成 搜尋 + 分頁 複合條件堆疊的需求於是在這個範例我實作了一個 keep directive 我知道這樣做會產生 side effect 並不是一個合理的做法,但是卻是一個最簡單的做法,求更好的思路

Vue.filter('keep', function (val, ref, method) {
  this[ref] = val[method]
  return val
})

另外有一個關於 filterBy 的小問題就是遇到自己客製的搜尋會跟 filterBy 結果不同,關於這點有些不是很明白為什麼在單個字元的過濾上會不一樣

source = this.source.filter((entry) => {
   var result = Object.keys(entry).map((key) => {
     if (typeof entry[key] === 'string') {
       return entry[key].toLowerCase().indexOf(query.toLowerCase()) > -1
     } else {
       return false
     }
   })
   return result.some((r) => r)
 })

下面為完整程式碼

  • App.vue

<style lang="sass" scoped>
.u-table-container {
  overflow: hidden;
  zoom: 1;
}

.u-tr {
  display: flex;
  flex-direction: row;

  .u-td:first-child, .u-th:first-child {
    border-left: none;
  }
}

.u-th {
  font-weight: 700;
}

.u-th,
.u-td {
  min-width: 120px;
  padding: 8px;
  border-left: 1px solid #ddd;
  border-bottom: 1px solid #ddd;
  word-wrap: break-word;
}

.u-table {
  border: 1px solid darken(#ddd, 10%);
}

.u-head {
  .u-th {
    border-bottom-color: darken(#ddd, 20%);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;


    &:hover {
      color: #636363;
    }

    &.scrollbar {
      flex: none;
      border-left: none;
      padding: 0;
      border-bottom: 1px solid darken(#ddd, 10%);
    }
  }
}

.u-body {
  overflow: auto;
  // overflow-y: scroll;
  max-height: 75vh;

  .u-tr {
    &:hover {
      background: lighten(#f3f3f3, 2%);
    }
  }
}

.arrow {
  display: inline-block;
  vertical-align: middle;
  width: 0;
  height: 0;
  margin-left: 5px;
  opacity: 0.66;
}

.arrow.asc {
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 4px solid #333;
}

.arrow.dsc {
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 4px solid #333;
}

.information {
  text-align: right;
  color: rgba(0, 0, 0, .57);
  font-size: .9em;
  padding-right: 5px;
}

.text-center {
  text-align: center;
}

.u-search-form {
  text-align: right;
  position: relative;

  .u-search {
    border-radius: 15px;
    border: 1px solid #ccc;
    margin-bottom: 5px;
    min-width: 180px;
    padding: 5px 20px;
  }

  .ion-ios-search {
    position: absolute;
    right: 10px;
    top: 5px;
    font-size: 1.2em;
  }

}

ul.pagination {
  display: inline-block;
  padding: 0;
  margin: 15px 0 0 0;
}

ul.pagination li {display: inline;}

ul.pagination li a {
  color: #999;
  float: left;
  padding: 8px 16px;
  text-decoration: none;
  border-top: 1px solid #ddd;
  border-bottom: 1px solid #ddd;
  border-right: 1px solid #ddd;
  
  &:first-child {
    border-left: 1px solid #ddd;
  }
}
ul.pagination li a.active {
    background-color: #4CAF50;
    color: white;
}

ul.pagination li a:hover:not(.active) {background-color: #ddd;}
</style>

<template>

<div class="u-table-container">
  <form id="search" class="u-search-form">
      <input name="query" v-model="searchQuery" class="u-search" placeholder="Search">
      <span class="icon ion-ios-search"></span>
  </form>

  <div class="u-table">
    <div class="u-head">
      <div class="u-tr" :style="{transform: `translate3d(${-this.left}px, 0, 0)`}">
        <div class="u-th"
          v-for="(index, column) in columns"
          :style="{ minWidth: column.width, flex: column.width ? 'none' : 1 }"
          @click.stop="sortBy($event, column.key)">

          <!-- slot head column -->
          <slot :name="'_' + index">{{ column.text }}</slot>

          <!-- sort button -->
          <span class="arrow" v-if="source[0][column.key]"
           :class="sortOrders[column.key] > 0 ? 'asc' : 'dsc'">

        </div>
        <div v-if="edge" class="u-th scrollbar" :style="{ minWidth: this.edge + 'px' }"></div>
      </div>
    </div>
    <div class="u-body" @scroll.stop.prevent="onHorizontalScroll">
      <div class="u-tr" v-for="entry in source | filterBy searchQuery | keep 'length' 'length' | orderBy sortKey sortOrders[sortKey] | limitBy size start">
        <div class="u-td" v-for="column in columns" :style="{ minWidth: column.width, flex: column.width ? 'none' : 1 }">
          <slot :name="entry.id + '_' + column.key">{{entry[column.key]}}</slot>
        </div>
      </div>
    </div>
  </div>

  <div class="information">Total {{source.length}} Records, Now {{ length }} Records, {{pages}} Pages</div>
  <div class="text-center">
    <ul class="pagination">
      <li @click.stop.prevent="jump('prev')">
        <a href="#" aria-label="Previous 10"><span aria-hidden="true" class="fa fa-angle-double-left"></span></a>
      </li>
      <li @click.stop.prevent="paginate(currentPage - 1)">
        <a href="#" aria-label="Previous"><span aria-hidden="true" class="fa fa-angle-left"></span></a>
      </li>
      <li v-for="n in scope" :class="{'active': n === this.currentPage}"><a href="#" @click.stop.prevent="paginate(n)">{{ n }}</a></li>
      <li @click.stop.prevent="paginate(currentPage + 1)">
        <a href="#" aria-label="Next"><span aria-hidden="true" class="fa fa-angle-right"></span></a>
      </li>
      <li @click.stop.prevent="jump('next')">
        <a href="#" aria-label="Next 10"><span aria-hidden="true" class="fa fa-angle-double-right"></span></a>
      </li>
    </ul>
  </div>
</div>

</template>

<script>

export default {
  props: {
    source: {
      type: Array
    },
    columns: {
      type: Array
    },
    // count per page
    size: {
      type: Number
    },
    paginatorSize: {
      type: Number,
      default: 10
    }
  },
  data () {
    var sortOrders = {}
    this.columns.forEach(function (column) {
      if (column.key)
        sortOrders[column.key] = 1
    })

    return {
      left: 0,        // header row offset position.
      edge: 0,        // offset of scrollbar width.
      sortKey: '',    // column name of sort
      sortOrders: sortOrders,
      currentPage: 1,
      searchQuery: null,
      length: this.source.length // total source count include filter result
    }
  },
  ready () {
    var clientWidth = document.querySelector('.u-body').clientWidth
    var offsetWidth = document.querySelector('.u-body').offsetWidth
    this.edge = offsetWidth - clientWidth;
  },
  methods: {
    onHorizontalScroll (e) {
      this.left = document.querySelector('.u-body').scrollLeft
    },
    sortBy (e, key) {
      this.sortKey = key
      this.sortOrders[key] = this.sortOrders[key] * -1
    },
    paginate (page) {
      if (page < 1) page = 1
      if (page > this.pages) page = this.pages
      this.currentPage = page
    },
    /**
     * jump to first page of N scope
     * @param  {String} dir [value is 'next' or 'prev', next or prev scope]
     */
    jump (dir = 'next') {
      var first = this.scope[0]
      var last = this.scope.slice(-1)[0]
      var page = this.currentPage // default

      switch (dir) {
        case 'next':
          page = (first + this.paginatorSize) > this.pages ? last : (first + this.paginatorSize)
          break;
        case 'prev':
          page = (first - this.paginatorSize) < 1 ? 1 : (first - this.paginatorSize)
          break;
        default:
          break;
      }
      this.currentPage = page
    }
  },
  computed: {
    start () {
      return (this.currentPage - 1) * this.size
    },
    // current pages e.g. current page is 1 then reutrn [1..10]
    scope () {
      var n = 1
      while (this.currentPage > this.paginatorSize * n) {
        n++
      }
      var startPage = this.paginatorSize * (n - 1) + 1
      var endPage = this.paginatorSize * n > this.pages ? this.pages : this.paginatorSize * n
      var arr = []
      for (var i = startPage; i <= endPage; i++) {
        arr.push(i)
      }
      return arr
    },
    pages () {
      var query = this.searchQuery
      var source = []
      var len = 0
      if (query) {
        // Using keep to save length by filter
        len = this.length
      } else {
        len = this.source.length
      }

      var pages = Math.ceil(len / this.size)
      if (this.currentPage > pages) {
        this.currentPage = 1
      }

      return pages
    }
  }
}
</script>
  • main.js

import Vue from 'vue'
import App from './App.vue'

Vue.filter('count', function (val) {
  if (!val) return 0
  var isArray = Array.isArray ? Array.isArray : (v) => {return v instanceof Array}
  return isArray(val) ? val.length :  Array.concat.call(null, val)
})

Vue.filter('keep', function (val, ref, method) {
  this[ref] = val[method]
  return val
})

new Vue({
  el: 'body',
  components: { App }
})
  • index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet">
  </head>
  <body>
    <app :source="[{name: 'A'}, {name: 'B'}, {name: 'C'}, {name: 'D'}, {name: 'B'}, {name: 'E'}, {name: 'F'}]" 
         :columns="[{key: 'name', text: '名稱'}]" 
         :paginator-size="10"
         :size="2" :></app>
    <script src="main.js"></script>
  </body>
</html>

另外一篇相同思維的文章但步驟比較詳細

回复
阅读 5.8k
1 个回答
Chobits
  • 7.3k
✓ 已被采纳
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏