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: stringdata: T
}
//apis文件下list.ts
import { http } from '@/utils'
//引入泛型
import type { ResType } from './shared'//  2. 定义具体的接口类型
export type ChannelItem = {id: numbername: 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.ItemonClick={() => goToDetail(item.art_id)}key={item.art_id}prefix={<Imagesrc={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: stringtitle: stringaut_id: stringcomm_count: numberpubdate: stringaut_name: stringis_top: numbercover: {type: numberimages: string[]}
}export type ListRes = {results: ListItem[]pre_timestamp: string
}type ReqParams = {channel_id: stringtimestamp: 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. 小于thresholdconst [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.Itemkey={item.art_id}prefix={<Imagesrc={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. 小于thresholdconst [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.ItemonClick={() => goToDetail(item.art_id)}key={item.art_id}prefix={<Imagesrc={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><divdangerouslySetInnerHTML={{__html: detail?.content,}}></div></div>)
}export default Detail

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯第九届省赛题-----彩灯控制系统笔记

题目要求&#xff1a; 一、 基本要求 1.1 使用 CT107D 单片机竞赛板&#xff0c;完成“彩灯控制器”功能的程序设计与调 试&#xff1b; 1.2 设计与调试过程中&#xff0c;可参考组委会提供的“资源数据包”&#xff1b; 1.3 Keil 工程文件以准考证号命名&#xff0c…

学成在线:课程计划绑定媒资文件

课程计划绑定媒资文件 当文件上传完成后其他模块可以访问媒资服务中的媒资文件&#xff0c;如给内容管理模块中的课程计划绑定对应的视频&#xff0c;文档文件 界面原型 教育机构用户进入课程管理页面并编辑某一个课程&#xff0c;在"课程大纲"标签页的某一小节后…

mysql 锁知识汇总

目录 一、锁1.1 什么是锁&#xff1f;1.2 全局锁1.2.1 定义1.2.2 应用场景1.2.3 会出现的问题1.2.4 解决方法 1.3 表级锁1.3.1 表锁1.3.2 元数据锁&#xff08;MDL&#xff09;1.3.3 意向锁1.3.4 AUTO-INC锁 1.4 行级锁1.4.1 记录锁(Record Lock)1.4.2 间隙锁(Gap Lock)1.4.3 N…

Python Moviepy 视频编辑踩坑实录2:音频如何修改为单通道

一、前言&#xff1a; 通过上一篇博文的处理&#xff0c;《Python Moviepy 视频编辑踩坑实录1&#xff1a;谁动了我的音频比特率》我们成功的把音频文件的音频采样率&#xff0c;成功的转化为了目标值&#xff1a;16000&#xff0c;但是接下来遇到了&#xff0c;下面的问题&am…

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;…

机器学习5-线性回归之损失函数

在线性回归中&#xff0c;我们通常使用最小二乘法&#xff08;Ordinary Least Squares, OLS&#xff09;来求解损失函数。线性回归的目标是找到一条直线&#xff0c;使得预测值与实际值的平方差最小化。 假设有数据集 其中 是输入特征&#xff0c; 是对应的输出。 线性回归的…

MongoDB索引详情

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

常见API

文章目录 Math类1.1 概述1.2 常见方法 System类2.1 概述2.2 常见方法 Runtime3.1 概述3.2 常见方法 Object类4.1 概述4.2 常见方法 Objects类5.1 概述5.2 常见方法 BigInteger类6.1 引入6.2 概述6.3 常见方法6.4 底层存储方式&#xff1a; 7 BigDecimal类7.1 引入7.2 概述7.3 常…

数据库连接池简介

顾名思义&#xff0c;数据库连接池本质上是个容器&#xff0c;负责分配和管理数据库连接——Connection&#xff0c;对标JDBC中的Conn对象。 一.简介 如果不存在连接池&#xff0c;则每次访问数据库时都需要建立新的连接对象&#xff0c;并在访问结束后销毁。长此以往会造成不…

COCO数据集介绍

COCO数据集介绍 什么是COCO数据集&#xff1f; COCO数据集是一个可用于图像检测、语义分割和图像标题生成的大规模数据集。它有超过330K张图像&#xff08;其中220K张是有标注的图像&#xff09;&#xff0c;包含150万个目标&#xff0c;80个目标类别&#xff08;行人、汽车、…

【DDD】学习笔记-识别限界上下文实践

先启阶段的领域场景分析是一个艰难的过程&#xff0c;我们要从纷繁复杂的业务需求细节中抽象出全部的领域场景&#xff0c;并通过剖析这些场景来获得一致的领域概念&#xff0c;提炼出主要的用户活动&#xff0c;并转换为用统一语言表达的领域行为。在这个过程中&#xff0c;用…

MySQL知识点总结(四)——MVCC

MySQL知识点总结&#xff08;四&#xff09;——MVCC 三个隐式字段row_idtrx_idroll_pointer undo logread viewMVCC与隔离级别的关系快照读和当前读 MVCC全称是Multi Version Concurrency Control&#xff0c;也就是多版本并发控制。它的作用是提高事务的并发度&#xff0c;通…