分页可以分为前端分页和后端分页
1.前端分页
前端分页就是先获取全部数据,数据少了还好,如果数据很大,获取数据的时间就会比较长;所以对于数据量大的操作,一般都采用后端分页的操作更合适。
下面是整个分页的模块
<template>
<div class="ys-table">
<YsDialog
v-model="isShowDialog"
:title="dialogTitle"
:close-on-click-mask="false"
:with-mask="true"
>
<template v-slot:content>
<Form
:id='id'
:params='selected'
@addSubmit='handleAddCallback'
@editSubmit='handleEditCallback'
/>
</template>
</YsDialog>
<i-row
type="flex"
justify="space-between"
align="bottom"
class="ys-table-toolbar"
>
<i-col class="ys-table-col">
<div>
<h4 class='table-title'>
带宽通道
</h4>
<i-button
@click="handleAdd"
>
<VIcon size="small">
add
</VIcon>
新建
</i-button>
</div>
</i-col>
<i-col v-if="searchAble">
<i-input
v-model="searchTemp"
search
placeholder="搜索"
@on-search="searchHandler"
/>
</i-col>
</i-row>
<Table
:data="data"
class="ys-table-body"
style="width: 100%"
>
<TableColumn
width="60"
label="序号"
prop="$index"
align="center"
fixed="left"
/>
<TableColumn
prop="name"
label="名称"
/>
<TableColumn label="整体限流">
<TableColumn
prop="extend_reference_mode"
label="引用方式"
/>
<TableColumn
label="最大带宽"
>
<template slot-scope="scope">
<div v-if="scope.row.traffic_limiting_mode === 'Stream'">
<div>
<VIcon size="small">
file_upload
</VIcon>
{{ scope.row.global_upstream_bandwidth_max }}
</div>
<div>
<VIcon size="small">
file_download
</VIcon>
{{ scope.row.global_downstream_bandwidth_max }}
</div>
</div>
<div v-else>
<span>{{ scope.row.global_total_bandwidth_max }}</span>
</div>
</template>
</TableColumn>
<TableColumn
label="保证带宽"
>
<template slot-scope="scope">
<div v-if="scope.row.traffic_limiting_mode === 'Stream'">
<div>
<VIcon size="small">
file_upload
</VIcon>
{{ scope.row.global_upstream_bandwidth_guaranteed }}
</div>
<div>
<VIcon size="small">
file_download
</VIcon>
{{ scope.row.global_downstream_bandwidth_guaranteed }}
</div>
</div>
<div v-else>
<span>{{ scope.row.global_total_bandwidth_guaranteed }}</span>
</div>
</template>
</TableColumn>
<TableColumn
prop="global_max_connections"
label="最大连接数"
/>
<TableColumn
prop="global_max_connection_rate"
label="最大连接速率"
/>
</TableColumn>
<TableColumn label="每IP限流">
<TableColumn
prop="extend_traffic_limiting_object"
label="限流对象"
/>
<TableColumn
label="最大带宽"
>
<template slot-scope="scope">
<div v-if="scope.row.traffic_limiting_mode === 'Stream'">
<div>
<VIcon size="small">
file_upload
</VIcon>
{{ scope.row.per_upstream_bandwidth_max }}
</div>
<div>
<VIcon size="small">
file_download
</VIcon>
{{ scope.row.per_downstream_bandwidth_max }}
</div>
</div>
<div v-else>
<span>{{ scope.row.per_total_bandwidth_max }}</span>
</div>
</template>
</TableColumn>
<TableColumn
label="保证带宽"
>
<template slot-scope="scope">
<div v-if="scope.row.traffic_limiting_mode === 'Stream'">
<div>
<VIcon size="small">
file_upload
</VIcon>
{{ scope.row.per_upstream_bandwidth_guaranteed }}
</div>
<div>
<VIcon size="small">
file_download
</VIcon>
{{ scope.row.per_downstream_bandwidth_guaranteed }}
</div>
</div>
<div v-else>
<span>{{ scope.row.per_total_bandwidth_guaranteed }}</span>
</div>
</template>
</TableColumn>
<TableColumn
prop="per_max_connections"
label="最大连接数"
/>
<TableColumn
prop="per_max_connection_rate"
label="最大连接速率"
/>
</TableColumn>
<TableColumn
label="操作"
width='100'
>
<template slot-scope="scope">
<i-button-group>
<i-button
size="small"
@click="handleEdit(scope.row)"
>
编辑
</i-button>
<i-button
size="small"
@click="handleDelete(scope.row)"
>
删除
</i-button>
</i-button-group>
</template>
</TableColumn>
<div
slot="empty"
class="status-panel"
:class="is_error && 'is-error'"
>
<template v-if="is_error">
<IIcon
type="md-warning"
class="icon"
/>
<span>{{ isErrorText }}</span>
</template>
<template v-else>
<IIcon
type="ios-copy-outline"
class="icon"
/>
<span>{{ noDataText }}</span>
</template>
</div>
</Table>
<i-row
v-if="total > 10 || pages > 1"
type="flex"
justify="end"
>
<i-col>
<i-page
:current="page"
:total="total"
show-total
show-sizer
class="ys-table-pagination"
@on-change="pageChangeHandler"
@on-page-size-change="sizeChangeHandler"
/>
</i-col>
</i-row>
<i-spin v-if="!!loading" />
</div>
</template>
<script>
import { bus, REFRESH_FW_SPEED_LIMIT } from 'common/bus'
import { Table, TableColumn } from 'element-ui'
import 'element-ui/lib/theme-chalk/table.css'
import adapter from 'base/components/Table/AsyncAdapter'
import { SPEED_LIMIT_REFERENCE_MODE } from 'common/enum'
import Form from './form'
export default {
name: 'Channel',
components: {
Table,
TableColumn,
Form
},
props: {
id: {
type: String,
default: ''
},
noDataText: {
type: String,
default: '暂无数据'
},
isErrorText: {
type: String,
default: '请求错误'
}
},
data() {
return {
searchTemp: '',
searchStr: '',
searchAble: true,
api: () => [],
selected: {},
dialogTitle: '',
isShowDialog: false,
loading: true,
is_error: false,
oriData: [],
data: [],
total: 0,
page: 1,
size: 10,
}
},
computed: {
pages() {
return Math.ceil(this.total / this.size)
},
},
watch: {
api: {
handler() {
this.fetchData()
},
immediate: true,
},
oriData: {
handler: _.throttle(function() {
this.flushTable()
}, 300),
deep: true
}
},
created() {
this.fetchChannel()
bus.$on(REFRESH_FW_SPEED_LIMIT, this.fetchChannel)
},
destroyed() {
bus.$off(REFRESH_FW_SPEED_LIMIT, this.fetchChannel)
},
methods: {
handleAddCallback() {
ys.success('新建成功')
this.isShowDialog = false
this.fetchChannel()
},
handleEditCallback() {
ys.success('修改成功')
this.isShowDialog = false
this.fetchChannel()
},
fetchChannel() {
this.api = adapter(this.$request('/fw/traffic-profile/get', {
urlQuery: {
resource_uuid: this.id,
}
}).then(data => {
return _.map(_.get(data, 'DATA', []), channel => {
const mode = _.find(SPEED_LIMIT_REFERENCE_MODE, mode => {
return mode.value === channel.reference_mode
})
channel.extend_reference_mode = _.get(mode, 'name') || '未知'
channel.extend_traffic_limiting_object =
channel.traffic_limiting_object === 'Ip' ? '每IP' : '未知'
return channel
})
}))
},
searchHandler() {
this.searchStr = this.searchTemp
this.fetchData()
},
handleAdd() {
this.selected = {}
this.isShowDialog = true
this.dialogTitle = '新增带宽通道'
},
handleEdit(row) {
this.selected = row
this.isShowDialog = true
this.dialogTitle = '修改带宽通道'
},
handleDelete(row) {
let self = this
self.$YsDialog.confirm({
title: '删除带宽通道',
content: '您确认要删除带宽通道 ' + row.name + ' ?',
onConfirm() {
return self.$request('/fw/traffic-profile/delete', {
urlQuery: {
uuid: row.uuid,
},
}).then(() => {
ys.success('删除成功');
self.fetchChannel();
})
}
})
},
pageChangeHandler(page) {
this.page = page
this.fetchData()
this.$emit('on-page-change', page)
},
sizeChangeHandler(size) {
this.page = 1
this.size = size
this.fetchData()
this.$emit('on-page-size-change', 1, size)
},
genQuery() {
const params = {
pagination: {
page: this.page,
size: this.size,
},
search: {
key: undefined,
value: this.searchStr
},
}
return params
},
fetchData() {
const fetchId = Math.random().toString()
this.loading = fetchId
this.is_error = false
const query = this.genQuery()
return Promise.resolve(this.api(query)).then(res => {
if (fetchId === this.loading) {
this.page = _.get(res, 'PAGE.INDEX', 1)
this.total = _.get(res, 'PAGE.TOTAL_ITEM', 0)
this.oriData = _.map(_.get(res, 'DATA', []), (d, i) => {
d.$index = (this.page - 1) * this.size + i + 1
return d
})
this.$emit('on-data-change', this.data)
this.flushTable()
}
}).catch(e => {
this.is_error = true
throw e
}).finally(() => {
if (this.loading === fetchId) {
this.loading = false
}
})
},
flushTable() {
this.data = this.oriData
},
}
}
</script>
<style lang="scss" scoped>
@import "~assets/css/variables.scss";
@include themify() {
.el-table ::v-deep .el-table__header th {
background-color: themed('table-header-bg-color');
}
.el-table--border ::v-deep .el-table__header {
@if $theme-name == dark {
th {
border-bottom: themed('border-base');
}
}
tr:last-child th:nth-last-child(2).is-leaf {
border-right: themed('border-base');
@if $theme-name != dark {
border-right-color: themed('background-color-darken');
}
}
}
.status-panel.is-error {
color: themed('error-color');
}
}
.ys-table {
position: relative;
.ys-table-toolbar {
padding: 0 0 10px;
.table-title {
padding: 0 0 10px;
}
}
.ys-table-pagination {
margin-top: 10px;
}
}
.status-panel {
display: flex;
justify-content: center;
align-items: center;
.icon {
font-size: 1.4em;
}
}
</style>
<style>
.ys-table-col .ivu-btn+.ivu-btn {
margin-left: 10px;
}
</style>
前端请求api接口拿到全部数据,做分页,详情如图
pageChangeHandler改变当前页码,重新展现数据
sizeChangeHandler每页展示多少条数据,并重新回到第一页展示
genQuery获取page,size,和search参数传给后端做搜索
2后端分页
下面这个是封装好的分页组件
<template>
<div class="ys-table">
<i-row
type="flex"
justify="space-between"
align="bottom"
class="ys-table-toolbar"
>
<i-col class="ys-table-col">
<slot name="operation" />
</i-col>
<i-col v-if="searchAble">
<i-input
v-model="search_tmp"
search
placeholder="搜索"
@on-search="searchHandler"
>
<YsSelect
v-if="searched_columns.length"
slot="prepend"
v-model="search_key"
:disabled="searched_columns.length === 1"
placeholder="全部"
clearable
class="search-column-select"
@on-clear="searchColClearHandler"
>
<i-option
v-for="col in searched_columns"
:key="col.key"
:value="col.key"
>
{{ col.title }}
</i-option>
</YsSelect>
</i-input>
</i-col>
</i-row>
<el-table
:columns='columns_reshaped'
:data='data'
:row-key="rowKey"
class="ys-table-body"
@sort-change="sortChangeHandler"
>
<div
slot="empty"
class="status-panel"
:class="is_error && 'is-error'"
>
<template v-if="is_error">
<IIcon
type="md-warning"
class="icon"
/>
<span>{{ isErrorText }}</span>
</template>
<template v-else>
<IIcon
type="ios-copy-outline"
class="icon"
/>
<span>{{ noDataText }}</span>
</template>
</div>
</el-table>
<i-row
v-if="pagination_total > 10 || pagination_pages > 1"
type="flex"
justify="end"
>
<i-col>
<i-page
:current="pagination_page"
:total="pagination_total"
:page-size="pagination_size"
show-total
show-sizer
class="ys-table-pagination"
@on-change="pageChangeHandler"
@on-page-size-change="sizeChangeHandler"
/>
</i-col>
</i-row>
<i-spin v-if="!!loading" />
</div>
</template>
<script>
import { Checkbox } from 'element-ui'
import ElTable from './ElTable'
export default {
name: 'YsTable',
components: {
ElTable
},
props: {
api: {
type: Function,
default: () => {}
},
columns: { // 列名,必须含 { key: '', title: ''} 的对象
type: Array,
required: true
},
rowKey: {
type: Boolean,
default: false
},
searchAble: {
type: Boolean,
default: true
},
defaultSearchCondition: {
type: Object,
default() {
return {
defaultSearchCol: '',
defaultSearchTemp: '',
}
}
},
noDataText: {
type: String,
default: '暂无数据'
},
isErrorText: {
type: String,
default: '请求错误'
},
namespace: {
type: String,
default: ''
}
},
data() {
return {
loading: true,
is_error: false,
ori_data: [],
data: [],
search_tmp: '',
search_value: '',
search_key: '',
pagination_total: 0,
pagination_page: 1,
pagination_size: 10,
order: {},
selection: [],
isSelectedAll: false,
isIndeterminated: false
}
},
computed: {
columns_reshaped() {
let hasIndex = false
const defaultIndex = {
key: '$index',
title: '序号',
width: 60,
align: 'center',
fixed: 'left'
}
const result = _.map(this.columns, col => {
if (col.type === 'index') {
hasIndex = true
return {
...defaultIndex,
...col
}
}
if (col.type === 'selection') {
return {
render: this.selectionTdRender,
headerRender: this.selectionThRender,
align: 'center',
width: 50,
...col,
type: null
}
}
return {
sortMethod: () => 0,
tooltip: true,
sortable: true,
...col,
}
})
return [].concat(hasIndex ? [] : defaultIndex, result)
},
searched_columns() {
return _.reduce(this.columns, (acc, col) => {
if (!('key' in col)) return acc
if (col.key.startsWith('$')) return acc
if (col.searchable) acc.push(col)
return acc
}, [])
},
pagination_pages() {
return Math.ceil(this.pagination_total / this.pagination_size)
},
},
watch: {
api() {
this.fetchData()
},
defaultSearchCondition: {
handler(val) {
if (val.defaultSearchCol && val.defaultSearchTemp) {
this.search_key = val.defaultSearchCol
this.search_tmp = val.defaultSearchTemp
this.searchHandler()
}
},
immediate: true,
deep: true
},
searched_columns: {
handler(v) {
if (v.length === 1) {
this.search_key = _.get(v, [0, 'key'], '')
} else {
this.search_key = ''
}
},
immediate: true
},
ori_data: {
handler: _.throttle(function() {
this.flushTable()
}, 300),
deep: true
},
},
created() {
if (this.$router) {
this.initPaginationFromQuery()
this.$watch('pagination_page', v => this.syncPaginationToQuery('page', v))
this.$watch('pagination_size', v => this.syncPaginationToQuery('size', v))
}
this.fetchData()
},
methods: {
searchHandler() {
this.search_value = this.search_tmp
this.fetchData()
},
searchColClearHandler() {
this.search_tmp = ''
this.searchHandler()
},
pageChangeHandler(page) {
if (page !== this.pagination_page) {
this.pagination_page = page
this.fetchData()
}
this.$emit('on-page-change', page)
},
sizeChangeHandler(size) {
if (size !== this.pagination_size) {
this.pagination_size = size
this.fetchData()
}
this.$emit('on-page-size-change', 1, size)
},
getFetchQuery() {
const params = {
pagination: {
page: this.pagination_page,
size: this.pagination_size,
},
order: this.order
}
if (this.searchAble) {
params.search = {
key: this.search_key,
value: this.search_value
}
if (!params.search.key) {
const searchCols = this.searched_columns.length ? this.searched_columns : this.columns
params.search = _.reduce(searchCols, (acc, col) => {
if ('key' in col) {
acc.push({
key: col.key,
value: this.search_value
})
}
return acc
}, [])
if (params.search.length === 1) {
params.search = params.search.pop()
}
}
}
return params
},
async fetchData() {
const fetchId = Math.random().toString()
this.loading = fetchId
this.is_error = false
const fetch_query = this.getFetchQuery()
try {
const res = await this.api(fetch_query)
if (fetchId !== this.loading) return
if (!res) return
this.pagination_page = _.get(res, 'PAGE.INDEX', 1)
this.pagination_total = _.get(res, 'PAGE.TOTAL_ITEM', 0)
this.ori_data = _.map(_.get(res, 'DATA', []), (d, i) => {
d.$index = (this.pagination_page - 1) * this.pagination_size + i + 1
return d
})
this.selection = _.reduce(this.ori_data, (acc, d) => {
if (d.$selected) {
acc.push(d.$index)
}
return acc
}, [])
this.$emit('on-data-change', this.data)
this.flushTable()
} catch (err) {
this.is_error = true
throw err
}
if (this.loading === fetchId) {
this.loading = false
}
},
flushTable() {
this.data = _.map(this.ori_data, d => {
d = { ...d }
d.$selected = this.selection.includes(d.$index)
return d
})
this.isSelectedAll = !_.isEmpty(this.data) && _.every(this.data, d => d.$selected)
this.isIndeterminated = !this.isSelectedAll && !_.isEmpty(this.selection)
},
sortChangeHandler({ key, order }) {
if (order === "asc") {
this.order = { key }
} else if (order === 'desc') {
this.order = {
key,
desc: true
}
} else {
this.order = {}
}
this.fetchData()
},
selectionClickHandler(selected, scoped) {
if (selected) {
this.selection.push(scoped.row.$index)
} else {
this.selection = _.filter(this.selection, s => {
return s !== scoped.row.$index
})
}
this.flushTable()
const selection = _.filter(this.ori_data, d => this.selection.includes(d.$index))
this.$emit('on-selection-change', selection)
const item = _.find(this.ori_data, d => d.$index === scoped.row.$index)
this.$emit('on-select', selection, item)
},
selectionAllClickHandler(scoped) {
if (this.isSelectedAll) {
this.selection = []
} else {
const selectable = _.get(scoped, ['column', 'selectable'], () => true)
this.selection = _.reduce(this.ori_data, (acc, d) => {
if (selectable(d)) {
acc.push(d.$index)
}
return acc
}, [])
}
this.flushTable()
const selection = _.filter(this.ori_data, d => this.selection.includes(d.$index))
this.$emit('on-selection-change', selection)
this.$emit('on-select-all', selection)
},
selectionTdRender(h, scoped) {
return h(Checkbox, {
props: {
value: _.get(scoped, ['row', '$selected'], false),
disabled: !_.get(scoped, ['column', 'selectable'], () => true)(scoped.row)
},
on: {
change: v => this.selectionClickHandler(v, scoped)
}
})
},
selectionThRender(h, scoped) {
return h(Checkbox, {
props: {
value: this.isSelectedAll,
indeterminate: this.isIndeterminated
},
on: {
change: () => this.selectionAllClickHandler(scoped)
}
})
},
initPaginationFromQuery() {
this.pagination_page = _.get(
this.$route?.query,
`${this.namespace}_page`,
this.pagination_page
) * 1
this.pagination_size = _.get(
this.$route?.query,
`${this.namespace}_size`,
this.pagination_size
) * 1
},
syncPaginationToQuery(key, value) {
this.$router.push({
query: {
...this.$route.query,
[`${this.namespace}_${key}`]: value
}
})
}
},
}
</script>
<style lang="scss" scoped>
@import "~assets/css/variables.scss";
@include themify() {
.status-panel.is-error {
color: themed(error-color);
}
}
.ys-table {
position: relative;
.ys-table-toolbar {
padding: 0 0 10px;
.search-column-select {
width: auto;
}
}
.ys-table-pagination {
margin-top: 10px;
}
}
.status-panel {
display: flex;
justify-content: center;
align-items: center;
.icon {
font-size: 1.4em;
}
}
</style>
<style>
.ys-table-col .ivu-btn+.ivu-btn {
margin-left: 10px;
}
</style>
后端做分页的话前端就只需要将页码page_index,每页数量page_size,做搜索的话将用户输入的数据传给后端,然后后端根据你传的值返回固定条数返回数据,前端展示到页面上。
菜鸟程序员,有错误欢迎指教