描述:
之前实现了自定义分页组件,但是由于现实业务需求(需要将过滤项表单、分页的offset和limit传递到路由中,即a?xxx=xxx&xxx=xxx
)。因此对分页组件进行了重新的设计。
分页组件的重新设计
之前的分页组件主要是监听每页显示数量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
}
最后
代码感觉还是不够精简,虽然实现了一定的流程化,但是感觉还有其它改进过程,望指正。。