分页组件+Table表格+编程式导航

描述:

之前实现了自定义分页组件,但是由于现实业务需求(需要将过滤项表单、分页的offset和limit传递到路由中,即a?xxx=xxx&xxx=xxx)。因此对分页组件进行了重新的设计。

分页组件的重新设计

上一篇:Quasar的自定义分页组件

之前的分页组件主要是监听每页显示数量limit和偏移量offset的变化来触发事件,进行回调,在回调处发起网络请求。

由于上述需求,在新的分页组件中主要监听了页面改变的监听事件,并通过页面改变监听,触发编程式导航,从而改变页面路由,通过对页面路由的更新来发起网络请求,从而实现目标。

<!--
  by nickctang
  自定义分页组件
  传入参数: pagination
  pagination包含偏移量、每页显示数量、和总数
  在进行分页切换是,触发编程式导航,导航到对应位置,并进行服务器请求,请求完毕后,将pagination回传回来
 -->
<template>
  <div class="row q-my-md fs-14" >
    <span class="self-center col-auto q-pb-sm q-mt-md q-mr-md">共{{paginationModel.count}}条,第{{ page }} /{{max}}页</span>
    <q-select
      class="col-auto self-center q-mr-md q-mt-md"
      style="width: 90px"
      v-model="paginationModel.limit"
      :options="perPageNumOptions"
    />
    <q-pagination class="col-auto q-mt-md" v-model="page"  :max="max" size="sm" boundary-links boundary-numbers :max-pages="10" direction-links ellipses @input="onPageChange"/>
  </div>
</template>

<script>

export default {
  name: 'CPagination',
  data () {
    return {
      perPageNumOptions: [
        {
          label: '每页12条',
          value: 12
        },
        {
          label: '每页24条',
          value: 24
        },
        {
          label: '每页48条',
          value: 48
        },
        {
          label: '每页96条',
          value: 96
        }
      ]
    }
  },
  computed: {
    page: {
      get: function () {
        if (this.paginationModel.count === 0) {
          return 0
        }
        // 偏移量/每页显示数量 向下取整 + 1
        return Math.floor(this.paginationModel.offset / this.paginationModel.limit) + 1
      },
      set: function (val) {
        // 页码改变动态的更新偏移量
        this.paginationModel.offset = (val - 1) * this.paginationModel.limit
      }
    },
    max: function () {
      // 最大页数,总数/每页显示数量,向上取整
      return Math.ceil(this.paginationModel.count / this.paginationModel.limit)
    },
    paginationModel: {// pagination的双向绑定,可令外界的pagination来改变page
      get: function () {
        return this.pagination
      },
      set: function (val) {
        this.$emit('paginationChange', val)
      }
    }
  },
  model: {
    prop: 'pagination',
    event: 'paginationChange'
  },
  methods: {
    onPageChange: function (val) {
      console.log('pagechange ' + val)
      const query = JSON.parse(JSON.stringify(this.$route.query)) // 深度复制
      query.limit = this.paginationModel.limit
      query.offset = this.paginationModel.offset
      this.$router.push(
        {
          path: this.$route.path,
          query: query
        })
    }
  },
  props: {
    pagination: {
      offset: 0,
      limit: 12,
      count: 0
    }
  }
}
</script>

在父组件使用该分页组件时,只需要传入pagination即可,pagination是一个JSON对象,包含offset、limit、count。

在页面进行网络请求得到结果后,只需要改变pagination的内容,即可动态的更新分页组件。

分页组件 + table + 编程式导航的结合使用

描述:
在父组件使用Table表格和分页组件的过程中,一般还会存在过滤项表单,来对数据进行过滤请求,这些过滤项和分页的offset、limit都需要当作路由的query参数。

实际上,该query参数模样,相当于axios进行get请求时的params。因此将表单对应组件的v-model和请求所需的param定义成相同的名称。

期间遇到的问题:
基本上还是多次请求的问题,在编程式导航过程中,如果没有对应的更新分页组件,由于count的变化,容易让分页组件响应编程式导航,而且由于没有更新offset、limit,从而导致编程式导航成功响应,进行了二次网络请求。

最终的解决:
对原来的分页组件进行的更新。在每次获取网络请求结束后,动态的改变pagination中的count、limit、offset,从而基本触发了编程式导航,但是由于路由的query没有改变,而不会响应beforeRouteUpdate,也就不会再次响应网络请求。

实现过程

  • 首先,定义table。
// html
<q-table
    no-data-label="没有更多数据"
    :data="tableData"
    :columns="columns"
    :pagination.sync="paginationControl"
    :loading="tableLoading"
    name="id"
    color="secondary" >
    ...
    //vue
    tableLoading: false, // 表格加载状态
    paginationControl: { rowsPerPage: 0 }, // 显示全部
    tableData: [],
    columns: [{
        name: 'severity',
        required: true,
        align: 'left',
        label: '问题级别',
        field: 'severity'
    },...
    ],
  • 其次,覆盖Table默认的分页,采用新的分页
// html
<template slot="bottom" slot-scope="props">
    <div class="col"></div>
    <c-pagination class="no-margin q-pb-sm" :pagination="pagination" />
</template>
// vue
// table+分页组件所需
pagination: {
    offset: 0,
    limit: 12,
    count: 0
}
  • 此外,如果对过滤项进行点击过滤,是进行的编程式路由导航操作,将过滤项格式化成为规定的路由query格式(为了保证和get请求params一致)。
// vue methods中
filterSearch: function () {
    this.$router.push({
        path: this.$route.path,
        query: filterToQueryFormat(this.filterForm) // 将过滤项格式化匹配到路由
    })
}
// filterToQueryFormat,引用的utils。包含了
/**
 * 筛选项格式化到url
 * @param {{筛选项表单}} filterForm
 * @return {{返回url的query}} query
 */
export function filterToQueryFormat (filterForm) {
      console.log('filterToQuery')
      let query = {}
      for (var key in filterForm) {
        if (key.endsWith('__in')) {
          query[key] = filterForm[key].toString()
        } else {
          query[key] = filterForm[key] || filterForm[key] === '' ? filterForm[key] : null
        }
      }
      query.offset = 0
      console.log(filterForm)
      console.log(query)
      console.log('filterToQuery end')
      return query
}
/**
 * url格式化匹配筛选项
 * @param {{url的query}} query
 * @param {{筛选项表单}}} filterFrom 由于是对象的引用,所以在此对filterForm进行改变即改变了外层的filterForm
 * @return {{返回url的query}} query
 */
export function queryTofilterFormat (query, filterForm) {
      console.log('queryToFilter')
      let filter = {}
      for (var key in filterForm) {
        if (key.endsWith('__in')) {
          filter[key] = query[key] ? query[key].split(',') : []
        } else {
          filter[key] = query[key] ? query[key] : null
        }
      }
      filter.limit = query.limit
      filter.offset = query.offset
      console.log(query)
      console.log(filter)
      console.log('queryToFilter end')
      return filter
}
  • 接下来,将过滤项表单定义为filterFrom,表单项所对应的具体组件v-model根据网络请求param一一对应。
// html
<q-input :clearable="filterForm.file__icontains!=null" v-model="filterForm.file__icontains" float-label="所属文件" />
q-btn color="secondary" size="sm" icon="search" label="筛选" @click="filterSearch" />
// vue
filterForm: { // 过滤项表单
    state__in: [],
    resolution__in: [],
    severity__in: [],
    is_tapdbug: null,
    file__icontains: null,
    author: null,
    ci_time__lte: null,
    ci_time__gte: null
}
  • 然后,每次进入页面,在mounted中进行第一次请求。对页面路由更新进行监听,从而发起请求。beforeRouteUpdate是只有页面路由的params或query进行改变才会触发,原页面复用,不进行页面刷新。
// vue
mounted () {
    // 加载数据
    this.tableRequest(this.$route.query)
 },
 beforeRouteUpdate (to, from, next) {
    // 路由query更新触发
    this.tableRequest(to.query)
    next()
  },
  • 最后,对数据请求的操作。
///
    /**
    * 表格数据请求
    * @param {{路由query参数}} query
    */
tableRequest: function (query) {
    this.tableLoading = true // 表格加载动画
    // 判断路由是否有limit和offset,如果没有,默认12、0
    query.limit = query.limit ? parseInt(query.limit) : 12
    query.offset = query.offset ? parseInt(query.offset) : 0
    issues(this.$route.params.project_id, query).then(response => {
    // 请求成功后,改变分页组件显示
    this.pagination = {
    limit: query.limit,
    offset: query.offset,
    count: response.count
    }
    // 将路由的query匹配到过滤项表单中
    this.filterForm = queryTofilterFormat(query, this.filterForm)
    // 返回数据
    this.tableData = response.results
    // 关闭table加载状态
    this.tableLoading = false
    }).catch(error => {
    this.tableLoading = false
    console.log(error)
    })
}

整个代码

// 分页组件
<!--
  by nickctang
  自定义分页组件
  传入参数: pagination
  pagination包含偏移量、每页显示数量、和总数
  在进行分页切换是,触发编程式导航,导航到对应位置,并进行服务器请求,请求完毕后,将pagination回传回来
 -->
<template>
  <div class="row q-my-md fs-14" >
    <span class="self-center col-auto q-pb-sm q-mt-md q-mr-md">共{{paginationModel.count}}条,第{{ page }} /{{max}}页</span>
    <q-select
      class="col-auto self-center q-mr-md q-mt-md"
      style="width: 90px"
      v-model="paginationModel.limit"
      :options="perPageNumOptions"
    />
    <q-pagination class="col-auto q-mt-md" v-model="page"  :max="max" size="sm" boundary-links boundary-numbers :max-pages="10" direction-links ellipses @input="onPageChange"/>
  </div>
</template>

<script>

export default {
  name: 'CPagination',
  data () {
    return {
      perPageNumOptions: [
        {
          label: '每页12条',
          value: 12
        },
        {
          label: '每页24条',
          value: 24
        },
        {
          label: '每页48条',
          value: 48
        },
        {
          label: '每页96条',
          value: 96
        }
      ]
    }
  },
  computed: {
    page: {
      get: function () {
        if (this.paginationModel.count === 0) {
          return 0
        }
        // 偏移量/每页显示数量 向下取整 + 1
        return Math.floor(this.paginationModel.offset / this.paginationModel.limit) + 1
      },
      set: function (val) {
        // 页码改变动态的更新偏移量
        this.paginationModel.offset = (val - 1) * this.paginationModel.limit
      }
    },
    max: function () {
      // 最大页数,总数/每页显示数量,向上取整
      return Math.ceil(this.paginationModel.count / this.paginationModel.limit)
    },
    paginationModel: {// pagination的双向绑定,可令外界的pagination来改变page
      get: function () {
        return this.pagination
      },
      set: function (val) {
        this.$emit('paginationChange', val)
      }
    }
  },
  model: {
    prop: 'pagination',
    event: 'paginationChange'
  },
  methods: {
    onPageChange: function (val) {
      console.log('pagechange ' + val)
      const query = JSON.parse(JSON.stringify(this.$route.query)) // 深度复制
      query.limit = this.paginationModel.limit
      query.offset = this.paginationModel.offset
      this.$router.push(
        {
          path: this.$route.path,
          query: query
        })
    }
  },
  props: {
    pagination: {
      offset: 0,
      limit: 12,
      count: 0
    }
  }
}
</script>
// issue_list
<template>
  <q-page>
    <proj-base-scroll content_class="p-max-w">
      <p class="q-title q-mb-md">
        <span class="q-mr-md">问题列表</span>
      <q-table
        no-data-label="没有更多数据"
        :data="tableData"
        :columns="columns"
        selection="multiple"
        :selected.sync="selectedSecond"
        :pagination.sync="paginationControl"
        :loading="tableLoading"
        name="id"
        color="secondary" >
        <q-tr slot="body" slot-scope="props" class="textPrimary" :props="props">
          <q-td auto-width>
            <q-checkbox color="secondary" v-model="props.selected" />
          </q-td>
          <q-td key="severity" :props="props">
            <q-chip dense square :color="CODELINT_STATUS.SEVERITY_CHOICES[props.row.severity].color">
              {{ CODELINT_STATUS.SEVERITY_CHOICES[props.row.severity].label }}
            </q-chip>
          </q-td>
          <q-td key="file" :props="props">
            <q-btn flat color="primary" size="sm" no-caps class="no-padding">
              {{ props.row.file.slice(props.row.file.lastIndexOf('/')) }}
            </q-btn>
            <q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">
                {{ props.row.file }}
            </q-tooltip>
          </q-td>
          <q-td key="revision" :props="props">
            <p class="q-mb-sm">
              {{ props.row.revision.substring(0, 8) }}
              <q-tooltip anchor="top middle" self="bottom middle" :offset="[10, 10]">
                {{ props.row.revision }}
              </q-tooltip>
            </p>
            <p class="no-margin textTertiary">{{ formatDatetime(props.row.ci_time) }}</p>
          </q-td>
          <q-td key="ruleinfo" :props="props">
            <p class="q-mb-sm">{{ props.row.checkrule__display_name }}</p>
            <p class="no-margin textTertiary">{{ props.row.msg }}</p>
          </q-td>
          <q-td key="state" :props="props">
            <q-chip dense square :color="CODELINT_STATUS.STATE_CHOICES[props.row.state].color">
              {{ CODELINT_STATUS.STATE_CHOICES[props.row.state].label }}
            </q-chip>
          </q-td>
          <q-td key="author" :props="props">{{ props.row.author }}</q-td>
          <q-td key="is_tapdbug" :props="props">{{ props.row.is_tapdbug?'已提单':'未提单' }}</q-td>
        </q-tr>
        <template slot="top" slot-scope="props">
          <div class="row col q-mb-md fs-14 gutter-sm">
            <div class="col-md-2 col-xs-4">
              <q-select
                multiple
                clearable
                v-model="filterForm.state__in"
                :options="CODELINT_STATUS.STATE_OPTIONS"
                float-label="状态"
              />
            </div>
            <div class="col-md-2 col-xs-4">
              <q-select
                multiple
                clearable
                v-model="filterForm.resolution__in"
                :options="CODELINT_STATUS.RESOLUTION_OPTIONS"
                float-label="解决方法"
              />
            </div>
            <div class="col-md-2 col-xs-4">
              <q-select
                multiple
                clearable
                v-model="filterForm.severity__in"
                :options="CODELINT_STATUS.SEVERITY_OPTIONS"
                float-label="问题级别"
              />
            </div>
            <div class="col-md-2 col-xs-4">
              <q-input :clearable="filterForm.file__icontains!=null" v-model="filterForm.file__icontains" float-label="所属文件" />
            </div>
            <div class="col-md-2 col-xs-4">
              <q-input :clearable="filterForm.author!=null" v-model="filterForm.author" float-label="责任人" />
            </div>
            <div class="col-md-2 col-xs-4">
              <q-select
                class="col-md-2 col-xs-4"
                clearable
                v-model="filterForm.is_tapdbug"
                :options="tapdOptions"
                float-label="xx情况"
              />
            </div>
            <div class="col-md-2 col-xs-4">
              <q-datetime float-label="引入时间>="  v-model="filterForm.ci_time__gte" type="datetime" clearable />
            </div>
            <div class="col-md-2 col-xs-4">
              <q-datetime float-label="引入时间<="  v-model="filterForm.ci_time__lte" type="datetime" clearable />
            </div>
            <div class="col self-center text-right">
              <q-btn color="secondary" :disabled="selectedSecond.length==0" size="sm" label="xxx" class="q-mr-md" />
              <q-btn color="secondary" size="sm" icon="search" label="筛选" @click="filterSearch" />
            </div>
          </div>
        </template>
        <template slot="bottom" slot-scope="props">
          <div class="col"></div>
          <c-pagination class="no-margin q-pb-sm" :pagination="pagination" />
        </template>
      </q-table>
    </proj-base-scroll>
  </q-page>
</template>

<style lang="stylus">
</style>

<script>
import projBaseScroll from 'src/components/codeproj/projBaseScroll'
import { CODELINT_STATUS } from '@/libs/status'
import { filterToQueryFormat, queryTofilterFormat } from '@/libs/utils'
import { mapState } from 'vuex'
import { date } from 'quasar'
import { issues } from '@/service/xxx'
export default {
  components: {
    projBaseScroll
  },
  data () {
    return {
      filterForm: { // 过滤项表单
        state__in: [],
        resolution__in: [],
        severity__in: [],
        is_tapdbug: null,
        file__icontains: null,
        author: null,
        ci_time__lte: null,
        ci_time__gte: null
      },
      // table+分页组件所需
      pagination: {
        offset: 0,
        limit: 12,
        count: 0
      },
      tableLoading: false, // 表格加载状态
      paginationControl: { rowsPerPage: 0 }, // 显示全部
      tableData: [],
      columns: [
       ....
      ],
      CODELINT_STATUS: CODELINT_STATUS,
      tapdOptions: [
        {
          label: '是',
          value: true
        }, {
          label: '否',
          value: false
        }
      ],
      selectedSecond: []
    }
  },
  mounted () {
    // 加载数据
    this.tableRequest(this.$route.query)
  },
  computed: mapState({
    project: state => state.codeproj.project
  }),
  beforeRouteUpdate (to, from, next) {
    // 路由query更新触发
    this.tableRequest(to.query)
    next()
  },
  methods: {
    filterSearch: function () {
      this.$router.push({
        path: this.$route.path,
        query: filterToQueryFormat(this.filterForm) // 将过滤项匹配到路由
      })
    },
    /**
     * 表格数据请求
     * @param {{路由query参数}} query
     */
    tableRequest: function (query) {
      this.tableLoading = true // 表格加载动画
      // 判断路由是否有limit和offset,如果没有,默认12、0
      query.limit = query.limit ? parseInt(query.limit) : 12
      query.offset = query.offset ? parseInt(query.offset) : 0
      issues(this.$route.params.project_id, query).then(response => {
        // 请求成功后,改变分页组件显示
        this.pagination = {
          limit: query.limit,
          offset: query.offset,
          count: response.count
        }
        // 将路由的query匹配到过滤项表单中
        this.filterForm = queryTofilterFormat(query, this.filterForm)
        // 返回数据
        this.tableData = response.results
        // 关闭table加载状态
        this.tableLoading = false
      }).catch(error => {
        this.tableLoading = false
        console.log(error)
      })
    },
    /**
     * 格式化时间
     */
    formatDatetime: function (datetime) {
      return date.formatDate(datetime, 'YYYY-MM-DD HH:mm')
    }
  }
}
</script>

// utils
/**
 * 筛选项格式化到url
 * @param {{筛选项表单}} filterForm
 * @return {{返回url的query}} query
 */
export function filterToQueryFormat (filterForm) {
  console.log('filterToQuery')
  let query = {}
  for (var key in filterForm) {
    if (key.endsWith('__in')) {
      query[key] = filterForm[key].toString()
    } else {
      query[key] = filterForm[key] || filterForm[key] === '' ? filterForm[key] : null
    }
  }
  query.offset = 0
  console.log(filterForm)
  console.log(query)
  console.log('filterToQuery end')
  return query
}

/**
 * url格式化匹配筛选项
 * @param {{url的query}} query
 * @param {{筛选项表单}}} filterFrom 由于是对象的引用,所以在此对filterForm进行改变即改变了外层的filterForm
 * @return {{返回url的query}} query
 */
export function queryTofilterFormat (query, filterForm) {
  console.log('queryToFilter')
  let filter = {}
  for (var key in filterForm) {
    if (key.endsWith('__in')) {
      filter[key] = query[key] ? query[key].split(',') : []
    } else {
      filter[key] = query[key] ? query[key] : null
    }
  }
  filter.limit = query.limit
  filter.offset = query.offset
  console.log(query)
  console.log(filter)
  console.log('queryToFilter end')
  return filter
}

最后

代码感觉还是不够精简,虽然实现了一定的流程化,但是感觉还有其它改进过程,望指正。。

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

推荐阅读更多精彩内容