react+ts+antd-mobile 动态tabs➕下拉加载

1.初始化项目

//搭建项目
npm create vite@latest react-jike-mobile -- --template react-ts
//安装依赖
npm i 
//运行
npm run dev

在这里插入图片描述

清理项目目录结构

在这里插入图片描述

安装ant design mobile

ant design mobile是ant design家族里专门针对于移动端的组件库

npm install --save antd-mobile
测试组件
import { Button } from 'antd-mobile'

function App() {
  return (
    <>
      <Button>click me </Button>
    </>
  )
}

export default App

2.初始化路由

react的路由初始化,采用react-router-dom进行配置
在这里插入图片描述

npm i react-router-dom

3. 配置基础路由

//List页面
const List = () => {
  return <div>this is List</div>
}

export default List
//detail页面
const Detail = () => {
  return <div>this is Detail</div>
}

export default Detail
//router文件下index.tsx
import { createBrowserRouter } from 'react-router-dom'
import List from '../pages/List'
import Detail from '../pages/Detail'

const router = createBrowserRouter([
  {
    path: '/',
    element: <List />,
  },
  {
    path: '/detail',
    element: <Detail />,
  },
])

export default router
//main.txt
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router/index.tsx'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <RouterProvider router={router} />
)

4. 配置路径别名

场景:项目中各个模块之间的互相导入导出,可以通过@别名路径做路径简化,经过配置@相当于src目录,比如:
在这里插入图片描述
步骤:
1.让vite做路径解析(真实的路径转换)
2.让vscode做智能路径提示(开发者体验)

1️⃣修改vite配置

//修改vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

2️⃣安装node类型包

npm i @types/node -D

3️⃣修改tsconfig.json文件

{
  "baseUrl": ".",
  "paths": {
    "@/*": [
      "src/*"
    ]
  },
}

5. 安装axios

1.安装axios到项目
2.在utils中封装http模块,主要包括接口基地址、超时时间、拦截器
3.在utils中做统一导出

//安装axios
npm i axios
// 封装axios在utils下http.ts里
import axios from 'axios'

const httpInstance = axios.create({
  baseURL: 'http://geek.itheima.net/v1_0',
  timeout: 5000,
})

// 拦截器
httpInstance.interceptors.request.use(
  (config) => {
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

httpInstance.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    return Promise.reject(error)
  }
)

export { httpInstance }

//utils下index.ts文件
// 模块中转导出文件
import { httpInstance } from './http'

export { httpInstance as http }

6.封装API模块—axios和ts的配合使用

场景:axios提供了request泛型方法,方便我们传入类型参数推导出接口返回值的类型
在这里插入图片描述
说明:泛型参数type的类型决定了res.data的类型
步骤:
1️⃣根据接口文档创建一个通用的泛型接口类型(多个接口返回值的结构是相似的)
2️⃣根据接口文档创建特有的接口类型(每个接口有自己特殊的数据格式)
3️⃣组合1和2的类型,得到最终传给request泛型的参数类型
在这里插入图片描述
在这里插入图片描述

//apis文件下shared.ts
// 1. 定义泛型
export type ResType<T> = {
  message: string
  data: T
}

//apis文件下list.ts
import { http } from '@/utils'
//引入泛型
import type { ResType } from './shared'

//  2. 定义具体的接口类型
export type ChannelItem = {
  id: number
  name: string
}

type ChannelRes = {
  channels: ChannelItem[]
}

// 请求频道列表

export function fetchChannelAPI() {
  return http.request<ResType<ChannelRes>>({
    url: '/channels',
  })
}

页面使用

import { fetchListAPI } from '@/apis/list'
fetchChannelAPI().then((res) => {
  console.log(res.data.data.channels)
})

7.home模块

在这里插入图片描述
在这里插入图片描述

Home模块—Tabs区域实现

实现步骤:
1️⃣使用ant-mobile组件库中的tabs组件进行页面结构的创建
2️⃣使用真实接口数据进行渲染
3️⃣有优化的点进行优化处理

在这里插入图片描述

Home模块—Tabs自定义hook函数优化

针对上面代码封装hook函数进行代码优化

场景:当前状态数据的各种操作逻辑和组件渲染是写在一起的,可以采用自定义hook封装的方式让逻辑和渲染相分离

实现步骤:
1️⃣把和tabs相关的响应式数据状态以及操作数据的方法放到hook函数中
2️⃣组件中调用hook函数,消费其返回的状态和方法

//home文件下useTabs.ts
import { useEffect, useState } from 'react'
import { ChannelItem, fetchChannelAPI } from '@/apis/list'

function useTabs() {
  const [channels, setChannels] = useState<ChannelItem[]>([])

  useEffect(() => {
    const getChannels = async () => {
      try {
        const res = await fetchChannelAPI()
        setChannels(res.data.data.channels)
      } catch (error) {
        throw new Error('fetch channel error')
      }
    }
    getChannels()
  }, [])

  return {
    channels,
  }
}

export { useTabs }

//home文件下 index.tsx
import './style.css'
import { Tabs } from 'antd-mobile'
import { useTabs } from './useTabs'
const Home = () => {
  const { channels } = useTabs()
  return (
    <div>
      <div className="tabContainer">
        {/* tab区域 */}
        <Tabs defaultActiveKey={'0'}>
          {channels.map((item) => (
            <Tabs.Tab title={item.name} key={item.id}>
            </Tabs.Tab>
          ))}
        </Tabs>
      </div>
    </div>
  )
}

export default Home

Home模块—List组件实现

实现步骤:
1️⃣搭建基础结构,并获取基础数据
2️⃣为组件设计channelld参数,点击tab时传入不同的参数
3️⃣实现上来加载功能

// home/homeList/index.tsx
import { Image, List } from 'antd-mobile'
// mock数据
// import { users } from './users'
import { useEffect, useState } from 'react'
import { ListRes, fetchListAPI } from '@/apis/list'
type Props = {
  channelId: string
}

const HomeList = (props: Props) => {
  const { channelId } = props
  // 获取列表数据
  const [listRes, setListRes] = useState<ListRes>({
    results: [],
    pre_timestamp: '' + new Date().getTime(),
  })

  useEffect(() => {
    const getList = async () => {
      try {
        const res = await fetchListAPI({
          channel_id: channelId,
          timestamp: '' + new Date().getTime(),
        })
        setListRes({
          results: res.data.data.results,
          pre_timestamp: res.data.data.pre_timestamp,
        })
      } catch (error) {
        throw new Error('fetch list error')
      }
    }
    getList()
  }, [channelId])

 

  return (
    <>
      <List>
        {listRes.results.map((item) => (
          <List.Item
            onClick={() => goToDetail(item.art_id)}
            key={item.art_id}
            prefix={
              <Image
                src={item.cover.images?.[0]}
                style={{ borderRadius: 20 }}
                fit="cover"
                width={40}
                height={40}
              />
            }
            description={item.pubdate}>
            {item.title}
          </List.Item>
        ))}
      </List>
   
    </>
  )
}

export default HomeList

// home/index.tsx
import './style.css'
import { Tabs } from 'antd-mobile'
import { useTabs } from './useTabs'
import HomeList from './HomeList'
const Home = () => {
  const { channels } = useTabs()
  return (
    <div>
      <div className="tabContainer">
        {/* tab区域 */}
        <Tabs defaultActiveKey={'0'}>
          {channels.map((item) => (
            <Tabs.Tab title={item.name} key={item.id}>
              {/* list组件 */}
              {/* 别忘嘞加上类名 严格控制滚动盒子 */}
              <div className="listContainer">
                <HomeList channelId={'' + item.id} />
              </div>
            </Tabs.Tab>
          ))}
        </Tabs>
      </div>
    </div>
  )
}

export default Home

// apis/list.ts
import { http } from '@/utils'

import type { ResType } from './shared'

//  2. 定义具体的接口类型
// 请求文章列表

type ListItem = {
  art_id: string
  title: string
  aut_id: string
  comm_count: number
  pubdate: string
  aut_name: string
  is_top: number
  cover: {
    type: number
    images: string[]
  }
}

export type ListRes = {
  results: ListItem[]
  pre_timestamp: string
}

type ReqParams = {
  channel_id: string
  timestamp: string
}

export function fetchListAPI(params: ReqParams) {
  return http.request<ResType<ListRes>>({
    url: '/articles',
    params,
  })
}

Home模块—List列表无限滚动实现

交互要求:List列表在滑动到底部时,自动加载下一页列表数据

实现思路:
1️⃣滑动到底部触发加载下一页动作

<InfiniteScroll>

2️⃣加载下一页数据
pre_timestamp 接口参数

3️⃣把老数据和新数据做拼接处理
[…oldList,…newList]

4️⃣停止监听边界值
hasMore

// home/homeList/index.tsx
import { Image, List, InfiniteScroll } from 'antd-mobile'
// mock数据
// import { users } from './users'
import { useEffect, useState } from 'react'
import { ListRes, fetchListAPI } from '@/apis/list'
import { useNavigate } from 'react-router-dom'

type Props = {
  channelId: string
}

const HomeList = (props: Props) => {
  const { channelId } = props
  // 获取列表数据
  const [listRes, setListRes] = useState<ListRes>({
    results: [],
    pre_timestamp: '' + new Date().getTime(),
  })

  useEffect(() => {
    const getList = async () => {
      try {
        const res = await fetchListAPI({
          channel_id: channelId,
          timestamp: '' + new Date().getTime(),
        })
        setListRes({
          results: res.data.data.results,
          pre_timestamp: res.data.data.pre_timestamp,
        })
      } catch (error) {
        throw new Error('fetch list error')
      }
    }
    getList()
  }, [channelId])

  // 开关 标记当前是否还有新数据
  // 上拉加载触发的必要条件:1. hasMore = true  2. 小于threshold
  const [hasMore, setHasMore] = useState(true)
  // 加载下一页的函数
  const loadMore = async () => {
    // 编写加载下一页的核心逻辑
    console.log('上拉加载触发了')
    try {
      const res = await fetchListAPI({
        channel_id: channelId,
        timestamp: listRes.pre_timestamp,
      })
      // 拼接新数据 + 存取下一次请求的时间戳
      setListRes({
        results: [...listRes.results, ...res.data.data.results],
        pre_timestamp: res.data.data.pre_timestamp,
      })
      // 停止监听
      if (res.data.data.results.length === 0) {
        setHasMore(false)
      }
    } catch (error) {
      throw new Error('fetch list error')
    }
    // setHasMore(false)
  }



  return (
    <>
      <List>
        {listRes.results.map((item) => (
          <List.Item
            key={item.art_id}
            prefix={
              <Image
                src={item.cover.images?.[0]}
                style={{ borderRadius: 20 }}
                fit="cover"
                width={40}
                height={40}
              />
            }
            description={item.pubdate}>
            {item.title}
          </List.Item>
        ))}
      </List>
      <InfiniteScroll loadMore={loadMore} hasMore={hasMore} threshold={10} />
    </>
  )
}

export default HomeList

8.详情模块-路由跳转&数据渲染

需求:点击列表中的某一项跳转到详情路由并显示当前文章

1️⃣通过路由跳转方法进行挑战,并传递参数
2️⃣在详情路由下获取参数,并请求数据
3️⃣渲染数据到页面中

在这里插入图片描述

// home/homeList/index.tsx
import { Image, List, InfiniteScroll } from 'antd-mobile'
// mock数据
// import { users } from './users'
import { useEffect, useState } from 'react'
import { ListRes, fetchListAPI } from '@/apis/list'
import { useNavigate } from 'react-router-dom'

type Props = {
  channelId: string
}

const HomeList = (props: Props) => {
  const { channelId } = props
  // 获取列表数据
  const [listRes, setListRes] = useState<ListRes>({
    results: [],
    pre_timestamp: '' + new Date().getTime(),
  })

  useEffect(() => {
    const getList = async () => {
      try {
        const res = await fetchListAPI({
          channel_id: channelId,
          timestamp: '' + new Date().getTime(),
        })
        setListRes({
          results: res.data.data.results,
          pre_timestamp: res.data.data.pre_timestamp,
        })
      } catch (error) {
        throw new Error('fetch list error')
      }
    }
    getList()
  }, [channelId])

  // 开关 标记当前是否还有新数据
  // 上拉加载触发的必要条件:1. hasMore = true  2. 小于threshold
  const [hasMore, setHasMore] = useState(true)
  // 加载下一页的函数
  const loadMore = async () => {
    // 编写加载下一页的核心逻辑
    console.log('上拉加载触发了')
    try {
      const res = await fetchListAPI({
        channel_id: channelId,
        timestamp: listRes.pre_timestamp,
      })
      // 拼接新数据 + 存取下一次请求的时间戳
      setListRes({
        results: [...listRes.results, ...res.data.data.results],
        pre_timestamp: res.data.data.pre_timestamp,
      })
      // 停止监听
      if (res.data.data.results.length === 0) {
        setHasMore(false)
      }
    } catch (error) {
      throw new Error('fetch list error')
    }
    // setHasMore(false)
  }

  const navigate = useNavigate()
  const goToDetail = (id: string) => {
    // 路由跳转
    navigate(`/detail?id=${id}`)
  }

  return (
    <>
      <List>
        {listRes.results.map((item) => (
          <List.Item
            onClick={() => goToDetail(item.art_id)}
            key={item.art_id}
            prefix={
              <Image
                src={item.cover.images?.[0]}
                style={{ borderRadius: 20 }}
                fit="cover"
                width={40}
                height={40}
              />
            }
            description={item.pubdate}>
            {item.title}
          </List.Item>
        ))}
      </List>
      <InfiniteScroll loadMore={loadMore} hasMore={hasMore} threshold={10} />
    </>
  )
}

export default HomeList

// apis/detail.ts
import { type ResType } from './shared'
import { http } from '@/utils'
/**
 * 响应数据
 */
export type DetailDataType = {
  /**
   * 文章id
   */
  art_id: string
  /**
   * 文章-是否被点赞,-1无态度, 0未点赞, 1点赞, 是当前登录用户对此文章的态度
   */
  attitude: number
  /**
   * 文章作者id
   */
  aut_id: string
  /**
   * 文章作者名
   */
  aut_name: string
  /**
   * 文章作者头像,无头像, 默认为null
   */
  aut_photo: string
  /**
   * 文章_评论总数
   */
  comm_count: number
  /**
   * 文章内容
   */
  content: string
  /**
   * 文章-是否被收藏,true(已收藏)false(未收藏)是登录的用户对此文章的收藏状态
   */
  is_collected: boolean
  /**
   * 文章作者-是否被关注,true(关注)false(未关注), 说的是当前登录用户对这个文章作者的关注状态
   */
  is_followed: boolean
  /**
   * 文章_点赞总数
   */
  like_count: number
  /**
   * 文章发布时间
   */
  pubdate: string
  /**
   * 文章_阅读总数
   */
  read_count: number
  /**
   * 文章标题
   */
  title: string
}

export function fetchDetailAPI(id: string) {
  return http.request<ResType<DetailDataType>>({
    url: `/articles/${id}`,
  })
}

// /detail/index.tsx
import { DetailDataType, fetchDetailAPI } from '@/apis/detail'
import { NavBar } from 'antd-mobile'
import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'

const Detail = () => {
  const [detail, setDetail] = useState<DetailDataType | null>(null)

  // 获取路由参数
  const [params] = useSearchParams()
  const id = params.get('id')
  useEffect(() => {
    const getDetail = async () => {
      try {
        const res = await fetchDetailAPI(id!)
        setDetail(res.data.data)
      } catch (error) {
        throw new Error('fetch detail error')
      }
    }
    getDetail()
  }, [id])

  const navigate = useNavigate()
  const back = () => {
    navigate(-1)
  }

  // 数据返回之前 loading渲染占位

  if (!detail) {
    return <div>this is loading...</div>
  }

  // 数据返回之后 正式渲染的内容
  return (
    <div>
      <NavBar onBack={back}>{detail?.title}</NavBar>
      <div
        dangerouslySetInnerHTML={{
          __html: detail?.content,
        }}></div>
    </div>
  )
}

export default Detail

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/368811.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

日志报错 git -c dif.mnemonicprefix=false -c core.guotepath=false 解决方法

前言: 在进行下面操作前,必须确保,你是否安装了Git。 查看Git 在命令行窗口中输入`git --version`: 如果这个命令成功显示了Git的版本信息,这表明Git已经被安装。 1. 使用Sourcetree SourceTree 是 Windows 和Mac OS X 下免费的 Git 和 Hg 客户端…

C++核心deque容器,stack容器,queue容器,list容器,set容器,pair ,map容器

3.deque容器 1.deque容器的基本概念 Vector容器是单向开口的连续内存空间&#xff0c;deque则是一种双向开口的连续线性空间。所谓的双向开口&#xff0c;意思是可以在头尾两端插入元素&#xff0c;但是在其头部操作效率奇差&#xff0c;无法被接受。 deque容器和vector容器最…

MongoDB索引详情

文章目录 MongoDB索引MongoDB索引数据结构WiredTiger数据文件在磁盘的存储结构 索引的分类索引设计原则索引操作创建索引查看索引删除索引 索引类型单键索引&#xff08;Single Field Indexes&#xff09;复合索引&#xff08;Compound Index&#xff09;多键索引&#xff08;M…

学成在线:采用XXL-JOB任务调度方案使用FFmpeg处理视频转码业务

分片技术方案 概述 XXL-JOB并不直接提供数据处理的功能&#xff0c;它只会给所有注册的执行器分配好分片序号&#xff0c;在向执行器下发任务调度的同时携带分片总数和当前分片序号等参数 设计作业分片方案保证多个执行器之间不会查询到重复的任务,保证任务不会重复执行 任…

机器学习-基础分类算法-KNN详解

KNN-k近邻算法 k-Nearest Neighbors 思想极度简单应用数学只是少效果好可以解释机器学习算法使用过程中的很多细节问题更完整的刻画机器学习应用的流程 创建简单测试用例 import numpy as np import matplotlib.pyplot as plt raw_data_X [[3.393533211, 2.331273381],[3.1…

Flutter实现轮播图功能

一、在pubspec.yaml中添加&#xff1a; dependencies:# 轮播图card_swiper: ^3.0.1card_swiper: ^3.0.1&#xff0c;要获取最新版本&#xff1a;https://pub-web.flutter-io.cn/packages/card_swiper/versions&#xff0c;这个里面有文档可以看&#xff0c;如下图&#xff1a;…

大模型ReAct智能体开发实战

哆啦A梦是很多人都熟悉的角色&#xff0c;包括我自己。 在成长过程中&#xff0c;我常常对他口袋里的许多小玩意感到惊讶&#xff0c;而且他知道何时使用它们。 随着大型语言模型 (LLM) 的发展趋势&#xff0c;你也可以构建一个具有相同行为方式的模型&#xff01; 我们将构建…

高中数学立体几何练习题3

用到的基础知识&#xff1a; 1. 2.

MATLAB计算多边形质心/矩心

前言&#xff1a;不规则四边形的中心 不规则四边形的出心有多种定义&#xff0c;以下是最常见的三种&#xff1a; 1.重心&#xff1a;重心是四边形内部所有顶点连线交点的平均位置。可以通过求解四个顶点坐标的平均值来找到重心。 2.质心&#xff1a;质心是四边形内部所有质点…

Python机器学习库(numpy库)

文章目录 Python机器学习库&#xff08;numpy库&#xff09;1. 数据的维度2. numpy基础知识2.1 numpy概述2.1 numpy概述2.1 numpy概述2.2 numpy库的引用 3. ndarray数组的创建3.1 N维数组对象ndarray3.2 创建ndarray数组3.2.1 使用Python列表、元组创建ndarray数组3.2.2 使用nu…

029 命令行传递参数

1.循环输出args字符串数组 public class D001 {public static void main(String[] args) {for (String arg : args) {System.out.println(arg);}} } 2.找打这个类的路径&#xff0c;打开cmd cmd C:\Users\Admin\IdeaProjects\JavaSE学习之路\scanner\src\com\yxm\demo 3. 编译…

Servlet+Ajax实现对数据的列表展示(极简入门)

目录 1.准备工作 1.数据库源&#xff08;这里以Mysql为例&#xff09; 2.映射实体类 3.模拟三层架构&#xff08;Dao、Service、Controller&#xff09; Dao接口 Dao实现 Service实现&#xff08;这里省略Service接口&#xff09; Controller层&#xff08;或叫Servlet层…

2024济南生物发酵展:会议日程安排和技术装备亮点预告

2024济南发酵展/2024生物发酵展/2024山东发酵展/2024济南生物制药展/2024生物技术展/2024食品设备展/2024食品加工展/2024济南细胞工程展 由中国生物发酵产业协会主办&#xff0c;上海信世展览服务有限公司承办的2024第12届国际生物发酵产品与技术装备展览会&#xff08;济南&a…

深入理解Istio服务网格数据平面Envoy

一、服务网格概述(service mesh) 在传统的微服务架构中&#xff0c;服务间的调用&#xff0c;业务代码需要考虑认证、熔断、服务发现等非业务能力&#xff0c;在某种程度上&#xff0c;表现出了一定的耦合性 服务网格追求高级别的服务流量治理能力&#xff0c;认证、熔断、服…

2023.12 淘天-数科 已offer

文章目录 岗位信息1面ld 12.17 1H2面 VP 12.18 40min3面 HR 12.2012.21offer薪资方案沟通 岗位信息 1面ld 12.17 1H &#xff08;是一个从业估计很长时间前辈&#xff0c;很平和&#xff0c;感觉能学到很多东西&#xff09; 自我介绍项目深究1.说下自己工作里最有成就感的事和…

图论练习3

内容&#xff1a;过程中视条件改变边权&#xff0c;利用树状数组区间加处理 卯酉东海道 题目链接 题目大意 个点&#xff0c;条有向边&#xff0c;每条边有颜色和费用总共有种颜色若当前颜色与要走的边颜色相同&#xff0c;则花费为若当前颜色与要走的边颜色不同&#xff0c;…

MYSQL——MySQL8.3无法启动

在新电脑上装了个MySQL&#xff0c;但是无法使用net start mysql启动&#xff0c;很是纳闷&#xff0c;使用mysqld --console去查看报错&#xff0c;也是没报错的&#xff0c;但是奇怪的是&#xff0c;我输入完这个mysqld --console之后&#xff0c;就等于启动了mysql了&#x…

第十一篇【传奇开心果系列】Python的OpenCV技术点案例示例:三维重建

传奇开心果短博文系列 系列短博文目录Python的OpenCV技术点案例示例系列短博文目录一、前言二、OpenCV三维重建介绍三、基于区域的SGBM示例代码四、BM(Block Matching)算法介绍和示例代码五、基于能量最小化的GC(Graph Cut)算法介绍和示例代码六、相机标定介绍和示例代码七…

【数据结构与算法】之排序系列-20240203

这里写目录标题 一、628. 三个数的最大乘积二、645. 错误的集合三、747. 至少是其他数字两倍的最大数四、905. 按奇偶排序数组五、922. 按奇偶排序数组 II六、976. 三角形的最大周长 一、628. 三个数的最大乘积 简单 给你一个整型数组 nums &#xff0c;在数组中找出由三个数组…

Leetcode刷题笔记题解(C++):36. 有效的数独

思路一&#xff1a;暴力破解&#xff0c;两个二维数组记录行、列对应的数字出现的次数&#xff0c;比如rows[i][index]表示的数字index在i行出现的次数&#xff0c;三维数组记录每个块中对应数字出现的次数&#xff0c;比如boxes[i/3][j/3][index]表示的数字index在[i/3][j/3]个…