Appearance
分页实现
后端实现
提示
后端采用Mybatis Plus 提供的插件实现,其中IService中已经封装好了单表分页查询,多表分页需要自己写sql实现。
- 配置分页插件
配置文件
yamlacuity: database: maxLimit: -1 # 单页分页条数限制 小于0表示不显示;大于0表示每页最多查询maxLimit条数据。 如:maxLimit = 10,分页参数pageSize 传20时,也只返回10条数据。 dbType: MYSQL # 数据库类型 overflow: true # 溢出总页数后是否进行处理 如:总共有3页,你查询第4页数据时,是否设置为第一页 optimizeJoin: true # 生成 countSql 优化掉 join 现在只支持 left join
acuity: database: maxLimit: -1 # 单页分页条数限制 小于0表示不显示;大于0表示每页最多查询maxLimit条数据。 如:maxLimit = 10,分页参数pageSize 传20时,也只返回10条数据。 dbType: MYSQL # 数据库类型 overflow: true # 溢出总页数后是否进行处理 如:总共有3页,你查询第4页数据时,是否设置为第一页 optimizeJoin: true # 生成 countSql 优化掉 join 现在只支持 left join
配置代码
javapublic abstract class BaseMybatisConfiguration { @Bean @Order(5) @ConditionalOnMissingBean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); //... // 分页插件 PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(); // 单页分页条数限制 paginationInterceptor.setMaxLimit(databaseProperties.getMaxLimit()); // 数据库类型 paginationInterceptor.setDbType(databaseProperties.getDbType()); // 溢出总页数后是否进行处理 paginationInterceptor.setOverflow(databaseProperties.getOverflow()); // 生成 countSql 优化掉 join 现在只支持 left join paginationInterceptor.setOptimizeJoin(databaseProperties.getOptimizeJoin()); interceptor.addInnerInterceptor(paginationInterceptor); } }
public abstract class BaseMybatisConfiguration { @Bean @Order(5) @ConditionalOnMissingBean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); //... // 分页插件 PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(); // 单页分页条数限制 paginationInterceptor.setMaxLimit(databaseProperties.getMaxLimit()); // 数据库类型 paginationInterceptor.setDbType(databaseProperties.getDbType()); // 溢出总页数后是否进行处理 paginationInterceptor.setOverflow(databaseProperties.getOverflow()); // 生成 countSql 优化掉 join 现在只支持 left join paginationInterceptor.setOptimizeJoin(databaseProperties.getOptimizeJoin()); interceptor.addInnerInterceptor(paginationInterceptor); } }
单表分页,只要继承了IService,均可直接使用:
java/** * 翻页查询 * * @param page 翻页对象 * @param queryWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.query.QueryWrapper} */ default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper) { return getBaseMapper().selectPage(page, queryWrapper); } /** * 无条件翻页查询 * * @param page 翻页对象 * @see Wrappers#emptyWrapper() */ default <E extends IPage<T>> E page(E page) { return page(page, Wrappers.emptyWrapper()); } /** * 翻页查询 * * @param page 翻页对象 * @param queryWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.query.QueryWrapper} */ default <E extends IPage<Map<String, Object>>> E pageMaps(E page, Wrapper<T> queryWrapper) { return getBaseMapper().selectMapsPage(page, queryWrapper); } /** * 无条件翻页查询 * * @param page 翻页对象 * @see Wrappers#emptyWrapper() */ default <E extends IPage<Map<String, Object>>> E pageMaps(E page) { return pageMaps(page, Wrappers.emptyWrapper()); }
/** * 翻页查询 * * @param page 翻页对象 * @param queryWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.query.QueryWrapper} */ default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper) { return getBaseMapper().selectPage(page, queryWrapper); } /** * 无条件翻页查询 * * @param page 翻页对象 * @see Wrappers#emptyWrapper() */ default <E extends IPage<T>> E page(E page) { return page(page, Wrappers.emptyWrapper()); } /** * 翻页查询 * * @param page 翻页对象 * @param queryWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.query.QueryWrapper} */ default <E extends IPage<Map<String, Object>>> E pageMaps(E page, Wrapper<T> queryWrapper) { return getBaseMapper().selectMapsPage(page, queryWrapper); } /** * 无条件翻页查询 * * @param page 翻页对象 * @see Wrappers#emptyWrapper() */ default <E extends IPage<Map<String, Object>>> E pageMaps(E page) { return pageMaps(page, Wrappers.emptyWrapper()); }
多表分页或自定义分页, 需要在自己的Mapper中写sql语句
UserMapper.java 方法内容
javapublic interface UserMapper {//可以继承或者不继承BaseMapper /** * <p> * 查询 : 根据state状态查询用户列表,分页显示 * </p> * * @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象) * @param state 状态 * @return 分页对象 */ IPage<User> selectPageVo(Page<?> page, Integer state); }
public interface UserMapper {//可以继承或者不继承BaseMapper /** * <p> * 查询 : 根据state状态查询用户列表,分页显示 * </p> * * @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象) * @param state 状态 * @return 分页对象 */ IPage<User> selectPageVo(Page<?> page, Integer state); }
UserMapper.xml 等同于编写一个普通 list 查询,mybatis-plus 自动替你分页
xml<select id="selectPageVo" resultType="com.baomidou.cloud.entity.UserVo"> SELECT id,name FROM user WHERE state=#{state} </select>
<select id="selectPageVo" resultType="com.baomidou.cloud.entity.UserVo"> SELECT id,name FROM user WHERE state=#{state} </select>
UserServiceImpl.java 调用分页方法
javapublic IPage<User> selectUserPage(Page<User> page, Integer state) { // 不进行 count sql 优化,解决 MP 无法自动优化 SQL 问题,这时候你需要自己查询 count 部分 // page.setOptimizeCountSql(false); // 当 total 为小于 0 或者设置 setSearchCount(false) 分页插件不会进行 count 查询 // 重点: 分页返回的对象与传入的对象是同一个! return userMapper.selectPageVo(page, state); }
public IPage<User> selectUserPage(Page<User> page, Integer state) { // 不进行 count sql 优化,解决 MP 无法自动优化 SQL 问题,这时候你需要自己查询 count 部分 // page.setOptimizeCountSql(false); // 当 total 为小于 0 或者设置 setSearchCount(false) 分页插件不会进行 count 查询 // 重点: 分页返回的对象与传入的对象是同一个! return userMapper.selectPageVo(page, state); }
QueryController 控制层封装了page 方法,接收
PageParams
格式的json参数,调用了query
方法后, 返回IPage
.java@ApiOperation(value = "分页列表查询") @PostMapping(value = "/page") @WebLog(value = "'分页列表查询:第' + #params?.current + '页, 显示' + #params?.size + '行'", response = false) default R<IPage<ResultVO>> page(@RequestBody @Validated PageParams<PageQuery> params) { IPage<Entity> page = query(params); IPage<ResultVO> voPage = BeanPlusUtil.toBeanPage(page, getResultVOClass()); // 处理查询后的分页结果, 如:调用EchoService回显字典、关联表数据等 【提供给子类重写】【无默认实现】 handlerResult(voPage); return success(voPage); } default IPage<Entity> query(PageParams<PageQuery> params) { // 处理查询参数,如:覆盖前端传递的 current、size、sort 等参数 以及 model 中的参数 【提供给子类重写】【无默认实现】 handlerQueryParams(params); // 构建分页参数(current、size)和排序字段等 IPage<Entity> page = params.buildPage(getEntityClass()); Entity model = BeanUtil.toBean(params.getModel(), getEntityClass()); // 根据前端传递的参数,构建查询条件【提供给子类重写】【有默认实现】 QueryWrap<Entity> wrapper = handlerWrapper(model, params); // 执行单表分页查询 getSuperService().page(page, wrapper); return page; }
@ApiOperation(value = "分页列表查询") @PostMapping(value = "/page") @WebLog(value = "'分页列表查询:第' + #params?.current + '页, 显示' + #params?.size + '行'", response = false) default R<IPage<ResultVO>> page(@RequestBody @Validated PageParams<PageQuery> params) { IPage<Entity> page = query(params); IPage<ResultVO> voPage = BeanPlusUtil.toBeanPage(page, getResultVOClass()); // 处理查询后的分页结果, 如:调用EchoService回显字典、关联表数据等 【提供给子类重写】【无默认实现】 handlerResult(voPage); return success(voPage); } default IPage<Entity> query(PageParams<PageQuery> params) { // 处理查询参数,如:覆盖前端传递的 current、size、sort 等参数 以及 model 中的参数 【提供给子类重写】【无默认实现】 handlerQueryParams(params); // 构建分页参数(current、size)和排序字段等 IPage<Entity> page = params.buildPage(getEntityClass()); Entity model = BeanUtil.toBean(params.getModel(), getEntityClass()); // 根据前端传递的参数,构建查询条件【提供给子类重写】【有默认实现】 QueryWrap<Entity> wrapper = handlerWrapper(model, params); // 执行单表分页查询 getSuperService().page(page, wrapper); return page; }
前端实现(基于vue-element)
将 el-pagination 封装成 Pagination 组件,在页面使用时参考如下代码:
typescript
<pagination
:limit.sync="queryParams.size"
:page.sync="queryParams.current"
:total="Number(tableData.total)"
@pagination="fetch"
v-show="tableData.total > 0"
/>
data() {
return {
queryParams: {
size: 10,
current: 1,
sort: 'id',
order: 'descending',
model: { },
extra: {},
},
tableData: {
total: 0
},
}
},
methods: {
fetch(params = {}) {
if (this.queryParams.timeRange) {
this.queryParams.extra.createTime_st = this.queryParams.timeRange[0];
this.queryParams.extra.createTime_ed = this.queryParams.timeRange[1];
}
this.queryParams.current = params.current ? params.current : this.queryParams.current;
this.queryParams.size = params.size ? params.size : this.queryParams.size;
userApi.page(this.queryParams).then(response => {
const res = response.data;
if (res.isSuccess) {
this.tableData = res.data;
}
});
},
}
<pagination
:limit.sync="queryParams.size"
:page.sync="queryParams.current"
:total="Number(tableData.total)"
@pagination="fetch"
v-show="tableData.total > 0"
/>
data() {
return {
queryParams: {
size: 10,
current: 1,
sort: 'id',
order: 'descending',
model: { },
extra: {},
},
tableData: {
total: 0
},
}
},
methods: {
fetch(params = {}) {
if (this.queryParams.timeRange) {
this.queryParams.extra.createTime_st = this.queryParams.timeRange[0];
this.queryParams.extra.createTime_ed = this.queryParams.timeRange[1];
}
this.queryParams.current = params.current ? params.current : this.queryParams.current;
this.queryParams.size = params.size ? params.size : this.queryParams.size;
userApi.page(this.queryParams).then(response => {
const res = response.data;
if (res.isSuccess) {
this.tableData = res.data;
}
});
},
}
前端实现(基于vben)
建议
vben大佬封装的框架讲解起来有点头晕,建议看 usePagination.tsx、pagination.ts、useTable.ts、 useDataSource.ts的代码可能理解得更深刻一些。
页面如何使用Table + 分页?
typescript// 表格 const [registerTable] = useTable({ api: page, // 请求后台提供的分页接口 beforeFetch: handleFetchParams, // handleFetchParams:主要是处理页面上的创建时间查询、Table表头上的筛选按钮参数 useSearchForm: true, // 是否在表单上方显示 搜索表单 });
// 表格 const [registerTable] = useTable({ api: page, // 请求后台提供的分页接口 beforeFetch: handleFetchParams, // handleFetchParams:主要是处理页面上的创建时间查询、Table表头上的筛选按钮参数 useSearchForm: true, // 是否在表单上方显示 搜索表单 });
后端接口
java
/*
* 入参一定要使用 @RequestBody PageParams<T> 格式
* 出参一定要使用 R<IPage<T>> 格式
*/
@PostMapping(value = "/page")
public R<IPage<DefUserResultVO>> page(@RequestBody @Validated PageParams<DefUserPageQuery> params) {
return success(superService.page(params));
}
/*
* 入参一定要使用 @RequestBody PageParams<T> 格式
* 出参一定要使用 R<IPage<T>> 格式
*/
@PostMapping(value = "/page")
public R<IPage<DefUserResultVO>> page(@RequestBody @Validated PageParams<DefUserPageQuery> params) {
return success(superService.page(params));
}
Table 分页请求后端接口核心代码 : useDataSource.ts
typescriptasync function fetch(opt?: FetchParams) { const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm, pagination } = unref(propsRef); // 判断api是否是 Function if (!api || !isFunction(api)) return; try { // 设置表格为加载状态 setLoading(true); const { pageField, sizeField, listField, totalField } = Object.assign( {}, FETCH_SETTING, fetchSetting, ); let pageParams: Recordable = {}; const { current = 1, pageSize = PAGE_SIZE } = unref(getPaginationInfo) as PaginationProps; if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) { pageParams = {}; } else { pageParams[pageField] = (opt && opt.page) || current; pageParams[sizeField] = pageSize; } const { sortInfo = {}, filterInfo } = searchState; // 合并 业务查询参数、分页参数、Table表头过滤参数、搜索表单参数 let params: Recordable = { ...pageParams, ...(useSearchForm ? getFieldsValue() : {}), ...searchInfo, ...(opt?.searchInfo ?? {}), ...sortInfo, ...filterInfo, ...(opt?.sortInfo ?? {}), ...(opt?.filterInfo ?? {}), }; /* * 请求后台之前,每个业务可能要处理一下查询参数 * 用法: useTable({ beforeFetch: (params) => { return params; }}) */ if (beforeFetch && isFunction(beforeFetch)) { params = (await beforeFetch(params)) || params; } // 调用后端分页接口,并获取分页数据 const res = await api(params); rawDataSourceRef.value = res; const isArrayResult = Array.isArray(res); let resultItems: Recordable[] = isArrayResult ? res : get(res, listField); const resultTotal: number = isArrayResult ? 0 : get(res, totalField); // 假如数据变少,导致总页数变少并小于当前选中页码,通过getPaginationRef获取到的页码是不正确的,需获取正确的页码再次执行 if (resultTotal) { const currentTotalPage = Math.ceil(resultTotal / pageSize); if (current > currentTotalPage) { setPagination({ current: currentTotalPage, }); fetch(opt); } } // 请求后台获取到分页数据之后,每个业务可能要处理一下返回参数。 比如:数据转换、过滤、清洗 if (afterFetch && isFunction(afterFetch)) { resultItems = (await afterFetch(resultItems)) || resultItems; } dataSourceRef.value = resultItems; setPagination({ total: resultTotal || 0, }); if (opt && opt.page) { setPagination({ current: opt.page || 1, }); } emit('fetch-success', { items: unref(resultItems), total: resultTotal, }); } catch (error) { emit('fetch-error', error); dataSourceRef.value = []; setPagination({ total: 0, }); } finally { setLoading(false); } }
async function fetch(opt?: FetchParams) { const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm, pagination } = unref(propsRef); // 判断api是否是 Function if (!api || !isFunction(api)) return; try { // 设置表格为加载状态 setLoading(true); const { pageField, sizeField, listField, totalField } = Object.assign( {}, FETCH_SETTING, fetchSetting, ); let pageParams: Recordable = {}; const { current = 1, pageSize = PAGE_SIZE } = unref(getPaginationInfo) as PaginationProps; if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) { pageParams = {}; } else { pageParams[pageField] = (opt && opt.page) || current; pageParams[sizeField] = pageSize; } const { sortInfo = {}, filterInfo } = searchState; // 合并 业务查询参数、分页参数、Table表头过滤参数、搜索表单参数 let params: Recordable = { ...pageParams, ...(useSearchForm ? getFieldsValue() : {}), ...searchInfo, ...(opt?.searchInfo ?? {}), ...sortInfo, ...filterInfo, ...(opt?.sortInfo ?? {}), ...(opt?.filterInfo ?? {}), }; /* * 请求后台之前,每个业务可能要处理一下查询参数 * 用法: useTable({ beforeFetch: (params) => { return params; }}) */ if (beforeFetch && isFunction(beforeFetch)) { params = (await beforeFetch(params)) || params; } // 调用后端分页接口,并获取分页数据 const res = await api(params); rawDataSourceRef.value = res; const isArrayResult = Array.isArray(res); let resultItems: Recordable[] = isArrayResult ? res : get(res, listField); const resultTotal: number = isArrayResult ? 0 : get(res, totalField); // 假如数据变少,导致总页数变少并小于当前选中页码,通过getPaginationRef获取到的页码是不正确的,需获取正确的页码再次执行 if (resultTotal) { const currentTotalPage = Math.ceil(resultTotal / pageSize); if (current > currentTotalPage) { setPagination({ current: currentTotalPage, }); fetch(opt); } } // 请求后台获取到分页数据之后,每个业务可能要处理一下返回参数。 比如:数据转换、过滤、清洗 if (afterFetch && isFunction(afterFetch)) { resultItems = (await afterFetch(resultItems)) || resultItems; } dataSourceRef.value = resultItems; setPagination({ total: resultTotal || 0, }); if (opt && opt.page) { setPagination({ current: opt.page || 1, }); } emit('fetch-success', { items: unref(resultItems), total: resultTotal, }); } catch (error) { emit('fetch-error', error); dataSourceRef.value = []; setPagination({ total: 0, }); } finally { setLoading(false); } }
调整Table的分页参数,可以修改 componentSetting.ts :
typescript// 表格配置 table: { // 表格接口请求通用配置,可在组件prop覆盖 // 支持 xxx.xxx.xxx格式 fetchSetting: { // 传给后台的当前页字段。 跟后台分页接口入参 PageParams 类的 current 字段一致 pageField: 'current', // 传给后台的每页显示多少条的字段。 跟后台分页接口入参 PageParams 类的 size 字段一致 sizeField: 'size', // 接口返回表格数据的字段 。 跟后台分页接口出参 IPage 类的 records 字段一致 listField: 'records', // 接口返回表格总数的字段 。 跟后台分页接口出参 IPage 类的 total 字段一致 totalField: 'total', // 传给后台用于排序的字段。 跟后台分页接口入参 PageParams 类的 sort 字段一致 sortField: SORT_FIELD, // 传给后台用于排序指定asc/desc的字段。 跟后台分页接口入参 PageParams 类的 order 字段一致 orderField: ORDER_FIELD, }, // 可选的分页选项 pageSizeOptions: ['20', '50', '80', '100'], // 默认每页显示多少条 defaultPageSize: 20, // 默认排序方法 defaultSortFn: (sortInfo: SorterResult) => { const { field, order } = sortInfo; return { // 排序字段 [SORT_FIELD]: field, // 排序方式 asc/desc [ORDER_FIELD]: order, }; }, // 自定义过滤方法 defaultFilterFn: (data: Partial<Recordable<string[]>>) => { return data; }, },
// 表格配置 table: { // 表格接口请求通用配置,可在组件prop覆盖 // 支持 xxx.xxx.xxx格式 fetchSetting: { // 传给后台的当前页字段。 跟后台分页接口入参 PageParams 类的 current 字段一致 pageField: 'current', // 传给后台的每页显示多少条的字段。 跟后台分页接口入参 PageParams 类的 size 字段一致 sizeField: 'size', // 接口返回表格数据的字段 。 跟后台分页接口出参 IPage 类的 records 字段一致 listField: 'records', // 接口返回表格总数的字段 。 跟后台分页接口出参 IPage 类的 total 字段一致 totalField: 'total', // 传给后台用于排序的字段。 跟后台分页接口入参 PageParams 类的 sort 字段一致 sortField: SORT_FIELD, // 传给后台用于排序指定asc/desc的字段。 跟后台分页接口入参 PageParams 类的 order 字段一致 orderField: ORDER_FIELD, }, // 可选的分页选项 pageSizeOptions: ['20', '50', '80', '100'], // 默认每页显示多少条 defaultPageSize: 20, // 默认排序方法 defaultSortFn: (sortInfo: SorterResult) => { const { field, order } = sortInfo; return { // 排序字段 [SORT_FIELD]: field, // 排序方式 asc/desc [ORDER_FIELD]: order, }; }, // 自定义过滤方法 defaultFilterFn: (data: Partial<Recordable<string[]>>) => { return data; }, },