Skip to content

分页实现

后端实现

提示

后端采用Mybatis Plus 提供的插件实现,其中IService中已经封装好了单表分页查询,多表分页需要自己写sql实现。

  1. 配置分页插件
  • 配置文件

    yaml
    acuity:
      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
  • 配置代码

    java
    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);
      }
    }
    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);
      }
    }
  1. 单表分页,只要继承了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());
     }
  2. 多表分页或自定义分页, 需要在自己的Mapper中写sql语句

  • UserMapper.java 方法内容

    java
    public 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 调用分页方法

    java
    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);
    }
    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);
    }
  1. 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

    typescript
    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);
    }
    }
    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;
    },
    },

欢迎使用天源云Saas快速开发系统