0803分页_加载更多-网络ajax请求2-react-仿低代码平台项目

文章目录

    • [1 分页](#1 分页)
      • [1.1 url与分页参数](#1.1 url与分页参数)
      • [1.2 分页组件与url](#1.2 分页组件与url)
      • [1.3 列表页引用分页组件](#1.3 列表页引用分页组件)
    • [2 加载更多](#2 加载更多)
      • [2.1 状态](#2.1 状态)
      • [2.2 触发时机](#2.2 触发时机)
      • [2.3 加载数据](#2.3 加载数据)
      • 2.4优化
    • 结语

1 分页

1.1 url与分页参数

查询问卷列表接口,添加分页参数:

  • page:当前页码(第几页)
  • pageSize:每页多少条记录

question.ts 查询文件接口列表参数扩展,如下所示:

ts 复制代码
type SearchOption = {
  keyword: string;
  isStar: boolean;
  isDeleted: boolean;
  page: number;
  pageSize: number;
};

浏览器url获取分页参数,useLoadQuestionListData.ts代码如下所示:

ts 复制代码
import { useSearchParams } from "react-router-dom";
import { useRequest } from "ahooks";
import { getQuestionListApi } from "@/api/question";

import {
  LIST_SEARCH_PARAM_KEY,
  LIST_PAGE_PARAM_KEY,
  LIST_PAGE_SIZE_PARAM_KEY,
  LIST_PAGE_SIZE_DEFAULT,
  LIST_PAGE_DEFAULT,
} from "@/constant";

type OptionType = {
  isStar: boolean;
  isDeleted: boolean;
  page: number;
  pageSize: number;
};

/**
 * 获取问卷列表
 * @returns 问卷列表
 */
function useLoadQuestionListData(opt: Partial<OptionType>) {
  const { isStar, isDeleted } = opt;
  const [searchParams] = useSearchParams();
  const keyword = searchParams.get(LIST_SEARCH_PARAM_KEY) || "";
  const page =
    parseInt(searchParams.get(LIST_PAGE_PARAM_KEY) || "") || LIST_PAGE_DEFAULT ;
  const pageSize =
    parseInt(searchParams.get(LIST_PAGE_SIZE_PARAM_KEY) || "") ||
    LIST_PAGE_SIZE_DEFAULT;

  async function load() {
    const data = await getQuestionListApi({
      keyword,
      isStar,
      isDeleted,
      page,
      pageSize,
    });
    //...
}

export default useLoadQuestionListData;

服务端解析分页参数,question.js代码如下:

js 复制代码
const Mock = require('mockjs')

const getQuestionList = require("./data/getQuestionList")

const Random = Mock.Random

module.exports = [
  // ...
  {
    // 获取问卷列表
    url: '/api/question',
    method: 'get',
    response(ctx) {
      const { query = {} } = ctx
      
      const isStar = query.isStar === 'true'
      const isDeleted = query.isDeleted === 'true'
      const page = parseInt(query.page) || 1
      const pageSize = parseInt(query.pageSize) || 10
      return {
        errno: 0,
        data: {
          list: getQuestionList({isStar, isDeleted, page, pageSize}),
          total: 100
        }
      }
    }
  },
]

1.2 分页组件与url

antd分页组件与url交互

由于多个列表页面都需要分页组件,这里我们以antd分页组件为基础,封装自定义分页组件,并且实现分页参数与url参数交互,ListPage.tsx代码如下所示:

tsx 复制代码
import { FC, useEffect, useState } from "react";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { Pagination } from "antd";

import {
  LIST_PAGE_DEFAULT,
  LIST_PAGE_PARAM_KEY,
  LIST_PAGE_SIZE_DEFAULT,
  LIST_PAGE_SIZE_PARAM_KEY,
} from "@/constant";

type PropsType = {
  total: number;
};

const ListPage: FC<PropsType> = (props: PropsType) => {
  const [current, setCurrent] = useState(LIST_PAGE_DEFAULT);
  const [pageSize, setPageSize] = useState(LIST_PAGE_SIZE_DEFAULT);

  const [searchParams] = useSearchParams();

  useEffect(() => {
    const curPage =
      parseInt(searchParams.get(LIST_PAGE_PARAM_KEY) || "") ||
      LIST_PAGE_DEFAULT;
    const curPageSize =
      parseInt(searchParams.get(LIST_PAGE_SIZE_PARAM_KEY) || "") ||
      LIST_PAGE_SIZE_DEFAULT;

    setCurrent(curPage);
    setPageSize(curPageSize);
  }, [searchParams]);

  const { total } = props;

  // 当page pageSize改变时,跳转页面(改变url)
  const nav = useNavigate();
  const { pathname } = useLocation();

  function handlePageChange(page: number, pageSize: number) {
    searchParams.set(LIST_PAGE_PARAM_KEY, page.toString());
    searchParams.set(LIST_PAGE_SIZE_PARAM_KEY, pageSize.toString());
    nav({
      pathname,
      search: searchParams.toString(),
    });
  }

  return (
    <Pagination
      current={current}
      pageSize={pageSize}
      total={total}
      onChange={handlePageChange}
    />
  );
};

export default ListPage;

1.3 列表页引用分页组件

"星标问卷"列表页Star.tsx代码如下:

tsx 复制代码
// ...
import ListPage from "@/components/ListPage";

const List: FC = () => {
  // ...
  //问卷列表数据
  const { data = {}, loading } = useLoadQuestionListData({ isStar: true });
  const { list = [], total = 0 } = data;
// ...
      <div className={styles.footer}>
        <ListPage total={total} />
      </div>
    </>
  );
};

export default List;

"回收站"列表页Trash.tsx代码如下所示:

tsx 复制代码
import { FC, useState } from "react";
import { useTitle } from "ahooks";
import ListPage from "../../components/ListPage";


const List: FC = () => {
  useTitle("调查问卷-回收站");

  //问卷列表数据
  const { data = {}, loading } = useLoadQuestionListData({ isDeleted: true });
  const { list = [], total = 0 } = data;
// ...
      <div className={styles.footer}>
        <ListPage total={total} />
      </div>
    </>
  );
};

export default List;

2 加载更多

2.1 状态

基础

  • page:当前页
  • list:全部数据列表,上划累加
  • total:总条数

计算项

  • hasMoreData:是否有更多数据

2.2 触发时机

  • 页面加载触发
  • 页面滚动到加载更多数据时触发
    • 监听页面滚动
    • 防抖处理
    • DOM计算页面滚动刀"加载更多"

2.3 加载数据

  • useRequest请求接口
  • 接口返回数据,设置状态

2.4优化

  • 加载更多,缓存处理
  • 刷新页面优化暂无数据
  • 搜索重置状态

完整"我的问卷"页List.tsx代码如下所示:

tsx 复制代码
import { FC, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { useDebounceFn, useRequest, useTitle } from "ahooks";
import { Typography, Spin, Empty } from "antd";

import QuestionCard from "@/components/QuestionCard";
import ListSearch from "@/components/ListSearch";
import styles from "./common.module.scss";
import { getQuestionListApi } from "@/api/question";
import { LIST_PAGE_SIZE_DEFAULT, LIST_SEARCH_PARAM_KEY } from "@/constant";

const { Title } = Typography;

const List: FC = () => {
  useTitle("调查问卷-我的问卷");

  //问卷列表数据
  const [started, setStarted] = useState(false);
  const [page, setPage] = useState(1);
  const [list, setList] = useState([]); // 全部列表数据,上划加载更多,累计
  const [total, setTotal] = useState(0);

  const hasMoreData = total > list.length;

  const [searchParams] = useSearchParams();
  const keyword = searchParams.get(LIST_SEARCH_PARAM_KEY) || "";

  // 搜索重置状态
  useEffect(() => {
    setStarted(false);
    setPage(1);
    setList([]);
    setTotal(0);
  }, [keyword]);

  // 加载数据
  const { run: loadData, loading } = useRequest(
    async () => {
      const data = await getQuestionListApi({
        page,
        pageSize: LIST_PAGE_SIZE_DEFAULT,
        keyword,
      });
      return data;
    },
    {
      manual: true,
      onSuccess(res) {
        const { list: newList = [], total = 0 } = res;
        // 累计数据
        setList(list.concat(newList));
        setTotal(total);
        setPage(page + 1);
      },
    }
  );
  // 尝试触发加载-防抖处理
  const containerRef = useRef<HTMLDivElement>(null);
  const { run: tryLoadMore } = useDebounceFn(
    () => {
      const elem = containerRef.current;
      if (elem == null) {
        return;
      }
      // 判断如果div bottom 小于等于页面的高度
      const domRect = elem.getBoundingClientRect();
      if (domRect == null) {
        return;
      }
      const { bottom } = domRect;
      if (bottom <= document.body.clientHeight) {
        // 加载数据
        loadData();
        setStarted(true);
      }
    },
    { wait: 500 }
  );

  // 触发时机:页面加载或者url参数(keyword)变化时
  useEffect(() => {
    // 第一次加载,初始化
    tryLoadMore();
  }, [searchParams]);

  // 监听页面滚动事件
  useEffect(() => {
    if (hasMoreData) {
      window.addEventListener("scroll", tryLoadMore);
    }

    return () => {
      // 解绑事件
      window.removeEventListener("scroll", tryLoadMore);
    };
  }, [searchParams, hasMoreData]);

  // 加载更多显示优化
  const LoadMoreContentElem = useMemo(() => {
    if (!started || loading) {
      return <Spin />;
    }
    if (total === 0) {
      return <Empty description="暂无数据" />;
    }
    if (!hasMoreData) {
      return <span>没有更多了......</span>;
    }
    return <span>开始加载下一页</span>;
  }, [started, loading, hasMoreData]);
  return (
    <>
      <div className={styles.header}>
        <div className={styles.left}>
          <Title level={3}>我的问卷</Title>
        </div>
        <div className={styles.right}>
          <ListSearch />
        </div>
      </div>
      <div className={styles.content}>
        {list.length > 0 &&
          list.map((q: any) => {
            const { _id } = q;
            return <QuestionCard key={_id} {...q} />;
          })}
      </div>
      <div className={styles.footer}>
        <div ref={containerRef}>{LoadMoreContentElem}</div>
      </div>
    </>
  );
};

export default List;

结语

❓QQ:806797785

⭐️仓库地址:https://gitee.com/gaogzhen

⭐️仓库地址:https://github.com/gaogzhen

1\][ahook官网](https://ahooks.js.org/)\[CP/OL\]. \[2\][mock文档](https://github.com/nuysoft/Mock/wiki/Getting-Started)\[CP/OL\]. \[3\][Ant Design官网](https://ant-design.antgroup.com/index-cn)\[CP/OL\].

相关推荐
vvilkim7 小时前
React 与 Vue 虚拟 DOM 实现原理深度对比:从理论到实践
前端·vue.js·react.js
小矮马9 小时前
React-组件和props
前端·javascript·react.js
懒羊羊我小弟9 小时前
React Router v7 从入门到精通指南
前端·react.js·前端框架
Mars狐狸10 小时前
AI项目改用服务端组件实现对话?包体积减小50%!
前端·react.js
吃面必吃蒜11 小时前
从 Vue 到 React:React 合成事件
javascript·vue.js·react.js
举个栗子dhy11 小时前
【血缘关系图下钻节点,节点展开收起功能,递归和迭代问题处理】
javascript·react.js
Aiolimp12 小时前
React中CSS使用方法
前端·react.js
Moment12 小时前
受控组件和非受控组件的区别?别再傻傻分不清了 😁😁😁
前端·javascript·react.js
数据智能老司机13 小时前
React关键概念——处理事件和状态
react.js·前端框架·前端工程化