项目制作流程

一、使用 CRA 创建项目

npx create-react-app name

二、按照业务规范整理项目目录 (重点src目录)

三、安装插件

npm install sass -Dnpm install antd --savenpm install react-router-dom

四、配置基础路由 Router

1. 安装路由包 react-router-dom

2. 准备两个基础路由组件 Layout 和 Login

3. 在 router/index.js 文件中引入组件进行路由配置,导出 router 实例

4. 在入口文件中渲染 <RouterProvider />,传入  router 实例

router/index.js

import { createBrowserRouter } from "react-router-dom";// 配置路由实例
const router = createBrowserRouter([{path: "/",element: <Layout />,},{path: "/login",element: <Login />,},
]);export default router;
import { RouterProvider } from "react-router-dom";
import router from "./router";const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<RouterProvider router={router} />);

五、登录

使用 AntD 现成的组件 创建登录页的内容结构

 主要组件:Card、Form、Input、Button

    <div><Card><img  />{/* 登录表单 */}<Form><Form.Item><Input /></Form.Item> <Form.Item><Input /></Form.Item> </Form></Card></div>

1. 表单校验实现

表单校验可以在提交登录之前,校验用户的输入是否符合预期,如果不符合就阻止提交,显示错误信息

    <Form.Itemlabel="Username"name="username"rules={[{ required: true, message: '请输入用户名!' }]}><Input size="large" placeholder="请输入手机号" /></Form.Item>

 增加失焦时校验

<Form validateTrigger="onBlur">...
</Form>

手机号为有效格式

          <Form.Itemname="mobile"// 多条校验逻辑 先校验第一条 第一条通过之后再校验第二条rules={[{required: true,message: '请输入手机号',},{pattern: /^1[3-9]\d{9}$/,message: '请输入正确的手机号格式'}]}><Input size="large" placeholder="请输入手机号" /></Form.Item>

2. 获取表单数据

当用户输入了正确的表单内容,点击确认按钮时,需要收集用户输入的内容,用来提交接口请求

解决方案:给 Form 组件绑定 onFinish 回调函数,通过回调函数的参数获取用户输入的内容

  const onFinish = async (values) => {console.log(values)}<Form onFinish={onFinish} validateTrigger="onBlur">...<Form.Item><Button type="primary" htmlType="submit" size="large" block>登录</Button></Form.Item></Form>

3. 封装 request 请求模块

 在整个项目中会发送很多网络请求,使用 axios 三方库做好统一封装,方便统一管理和复用

npm i axios

 utils/request.js

// axios的封装处理
import axios from "axios"// 1. 根域名配置
// 2. 超时时间
// 3. 请求拦截器 / 响应拦截器const request = axios.create({baseURL: 'http://geek.itheima.net/v1_0',timeout: 5000
})// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {return config
}, (error) => {return Promise.reject(error)
})// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {// 2xx 范围内的状态码都会触发该函数。// 对响应数据做点什么return response.data
}, (error) => {// 超出 2xx 范围的状态码都会触发该函数。// 对响应错误做点什么return Promise.reject(error)
})export { request }

utils/index.js

// 统一中转工具模块函数
// import {request} from '@/utils'import { request } from './request'export {request,
}

4. 使用 redux 管理 token

Token 作为一个用户的标识数据,需要在很多个模块中共享,Redux 可以方便的解决共享问题

(1)redux 中编写获取 Token 的 异步获取和同步修改

(2)Login 组件负责提交 action 并且把表单数据传递过来

npm i react-redux @reduxjs/toolkit

store/modules/user.js

// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken } from '@/utils'const userStore = createSlice({name: "user",// 数据状态initialState: {token: getToken() || '',},// 同步修改方法reducers: {setToken (state, action) {state.token = action.payload_setToken(action.payload)},}
})// 解构出actionCreater
const { setToken } = userStore.actions// 获取reducer函数
const userReducer = userStore.reducer// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {return async (dispatch) => {// 1. 发送异步请求const res = await request.post('authorizations', loginForm)// 2. 提交同步 action 进行 token 存入dispatch(setToken(res.data.token))}
}export { fetchLogin, setToken }export default userReducer

store/index.js

// 组合redux子模块 + 导出store实例
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'export default configureStore({reducer: {user: userReducer}
})

index.js

import { RouterProvider } from 'react-router-dom'
import router from './router'
import { Provider } from 'react-redux'
import store from './store'const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<Provider store={store}><RouterProvider router={router} /></Provider>
)

封装 localStorage - Token 持久化

现存问题:

Redux 存入 Token 之后如果刷新浏览器,Token 会丢失(持久化就是防止刷新时丢失 Token)

问题原因:

Redux 是基于浏览器内存的储存方式,刷新时状态恢复为初始值

utils/token.js

// 封装基于ls存取删三个方法
const TOKENKEY = 'token_key'function setToken (token) {return localStorage.setItem(TOKENKEY, token)
}function getToken () {return localStorage.getItem(TOKENKEY)
}function removeToken () {return localStorage.removeItem(TOKENKEY)
}export {setToken,getToken,removeToken
}

utils/index.js

// 统一中转工具模块函数
// import {request} from '@/utils'import { request } from './request'
import { setToken, getToken, removeToken } from './token'export {request,setToken,getToken,removeToken
}

pages/login/index.js

import { useDispatch } from 'react-redux'
import { fetchLogin } from '@/store/modules/user'
import { useNavigate } from 'react-router-dom'const Login = () => {const dispatch = useDispatch()const navigate = useNavigate()const onFinish = async (values) => {console.log(values)// 触发异步action fetchLoginawait dispatch(fetchLogin(values))// 1. 跳转到首页navigate('/')// 2. 提示一下用户message.success('登录成功')}return (...)
}

5. Axios 请求拦截器注入 Token

Token 作为用户的一个标识数据,后端很多接口都会以它作为接口权限判断的依据;请求拦截器注入 Token 之后,所有用到 Axios 实例的接口请求都自动携带了 Token

utils/request.js

import { getToken } from "./token"// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {// 操作这个config 注入token数据// 1. 获取到token// 2. 按照后端的格式要求做token拼接const token = getToken()if (token) {config.headers.Authorization = `Bearer ${token}`}return config
}, (error) => {return Promise.reject(error)
})

6. 使用 Token 做路由权限控制

有些路由页面的内容信息比较敏感,如果用户没有经过登录获取到有效 Token,是没有权限跳转的,根据 Token 的有无控制当前路由是否可以跳转,就是路由的权限控制

components/AuthRoute.js

// 封装高阶组件
// 核心逻辑: 有token 正常跳转  无token 去登录import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'export function AuthRoute ({ children }) {const token = getToken()if (token) {return <>{children}</>} else {return <Navigate to={'/login'} replace />}
}

router/index.js

import { createBrowserRouter } from 'react-router-dom'
import { AuthRoute } from '@/components/AuthRoute'// 配置路由实例
const router = createBrowserRouter([{path: "/",element: <AuthRoute> <Layout /></AuthRoute>,},{path: "/login",element: <Login />}
])export default router

六、Layout

1. 样式初始化

样式 reset

npm install normalize.css

index.js

import 'normalize.css'const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<Provider store={store}><RouterProvider router={router} /></Provider>
)

index.scss

html,
body {margin: 0;height: 100%;
}#root {height: 100%;
}

2.  二级路由配置

(1)准备三个二级路由

(2)router 中通过 children 配置项进行配置

(3)Layout 组件中配置二级路由出口

router/index.js

const router = createBrowserRouter([{path: "/",element: <AuthRoute> <Layout /></AuthRoute>,children: [{index: true,element: <Home />},{path: 'article',element: <Article />},{path: 'publish',element: <Publish />}]},{path: "/login",element: <Login />}
])export default router

pages/Layout/index.js

    <Layout><Header>...</Header><Layout><Sider><Menu></Menu></Sider><Layout style={{ padding: 20 }}>{/* 二级路由的出口 */}<Outlet /></Layout></Layout></Layout>

3. 点击菜单跳转路由

实现效果:点击左侧菜单可以跳转到对应的目标路由

思路分析:

(1)左侧菜单要和路由形成一一对应的关系

(2)点击时拿到路由路径,调用路由方法跳转(跳转到对应的路由下面)

 具体操作:

(1)菜单参数 Item 中 key 属性换成路由的路由地址

(2)点击菜单时通过 key 获取路由地址跳转

 pages/Layout/index.js

const items = [{label: '首页',key: '/',icon: <HomeOutlined />,},{label: '文章管理',key: '/article',icon: <DiffOutlined />,},{label: '创建文章',key: '/publish',icon: <EditOutlined />,},
]const GeekLayout = () => {const navigate = useNavigate()const onMenuClick = (route) => {const path = route.keynavigate(path)}return (<Layout><Header className="header">...</Header><Layout><Sider><Menumode="inline"theme="dark"defaultSelectedKeys={['1']}onClick={onMenuClick}items={items}style={{ height: '100%', borderRight: 0 }}></Menu></Sider><Layout>{/* 二级路由的出口 */}<Outlet /></Layout></Layout></Layout>)
}
export default GeekLayout

4. 根据当前路由路径高亮菜单

 实现效果:页面在刷新时可以根据当前的路由路径让对应的左侧菜单高亮显示

思路分析;

(1)获取当前 url 上的路由路径

(2)找到菜单组件负责高亮的属性,绑定当前的路由路径

  // 反向高亮// 1. 获取当前路由路径const location = useLocation()console.log(location.pathname)const selectedkey = location.pathname<Sider width={200} className="site-layout-background"><Menumode="inline"theme="dark"// 修改selectedKeys={selectedkey}onClick={onMenuClick}items={items}style={{ height: '100%', borderRight: 0 }}></Menu></Sider>

5. 展示个人信息

关键问题:用户信息应该放到哪里维护?

和 Token 令牌类似,用户的信息通常很有可能在多个组件中都需要共享使用,所以同样应该放到Redux 中维护

(1)使用 Redux 进行信息管理

(2)Layout 组件中提交 action

(3)Layout 组件中完成渲染

store/modules/user.js

// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken, removeToken } from '@/utils'
import { loginAPI, getProfileAPI } from '@/apis/user'const userStore = createSlice({name: "user",// 数据状态initialState: {token: getToken() || '',userInfo: {}},// 同步修改方法reducers: {setToken (state, action) {state.token = action.payload_setToken(action.payload)},setUserInfo (state, action) {state.userInfo = action.payload},}
})// 解构出actionCreater
const { setToken, setUserInfo } = userStore.actions// 获取reducer函数
const userReducer = userStore.reducer// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {return async (dispatch) => {const res = await request.post('/authorizations', loginForm)dispatch(setToken(res.data.token))}
}// 获取个人用户信息异步方法
const fetchUserInfo = () => {return async (dispatch) => {const res = await request.get('/user/profile')dispatch(setUserInfo(res.data))}
}export { fetchLogin, fetchUserInfo }export default userReducer

pages/Layout/index.js

  // 触发个人用户信息actionconst dispatch = useDispatch()useEffect(() => {dispatch(fetchUserInfo())}, [dispatch])const name = useSelector(state => state.user.userInfo.name)<span className="user-name">{name}</span>

6. 退出登录

(1)提示用户是否确认要退出(危险操作,二次确认)

(2)用户确认之后清除用户信息(Token 以及其他个人信息)

(3)跳转到登录页(为下次登录做准备)

pages/Layout/index.js

  // 退出登录确认回调const onConfirm = () => {console.log('确认退出')dispatch(clearUserInfo())navigate('/login')}...<span className="user-logout"><Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={onConfirm}><LogoutOutlined /> 退出</Popconfirm></span>

store/modules/user.js

  // 同步修改方法reducers: {setToken (state, action) {state.token = action.payload_setToken(action.payload)},setUserInfo (state, action) {state.userInfo = action.payload},clearUserInfo (state) {state.token = ''state.userInfo = {}removeToken()}}
})

7. 处理 Token 失效

什么是 Token 失效?

为了用户的安全和隐私考虑,在用户长时间未在网络中做出任何操作规定的失效时间到达之后,当前的 Token 就会失效。一旦失效,不能再作为用户令牌标识请求隐私数据

前端如何知道 Token 已经失效了?

通常在 Token 失效之后再去请求接口,后端会返回401状态码,前端可以监控这个状态,做后续的操作

Token 失效了前端做什么?

(1)在 axios 拦截中监控 401 状态码

(2)清除失效 Token, 跳转登录

utils/request.js

import router from "@/router"// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {// 2xx 范围内的状态码都会触发该函数。// 对响应数据做点什么return response.data
}, (error) => {// 超出 2xx 范围的状态码都会触发该函数。// 对响应错误做点什么// 监控 401 token失效console.dir(error)if (error.response.status === 401) {removeToken()router.navigate('/login')// 强制刷新window.location.reload()}return Promise.reject(error)
})

七、Home

1. Echarts 基础图表渲染

三方图表插件如何在项目中快速使用起来?

(1)按照图表插件文档中的“快速开始”,快速跑起来 Demo

(2)按照业务需求修改配置项做定制处理

npm install echarts

pages/Home/index.js

// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'const Home = () => {const chartRef = useRef(null)useEffect(() => {// 保证dom可用 才进行图表的渲染// 1. 获取渲染图表的dom节点const chartDom = chartRef.current// 2. 图表初始化生成图表实例对象const myChart = echarts.init(chartDom)// 3. 准备图表参数const option = {title: {text: title},xAxis: {type: 'category',data: ['Vue', 'React', 'Angular']},yAxis: {type: 'value'},series: [{data: [10, 40, 70],type: 'bar'}]}// 4. 使用图表参数完成图表的渲染option && myChart.setOption(option)}, [title])return (<div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>)
}export default Home

2. Echarts 组件封装实现

pages/Home/index.js 

import BarChart from "./components/BarChart"const Home = () => {return (<div><BarChart title={'三大框架满意度'} /><BarChart title={'三大框架使用度'} /></div>)
}export default Home

pages/Home/components/BarCharts.js

// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'
// 1. 把功能代码都放到这个组件中
// 2. 把可变的部分抽象成prop参数const BarChart = ({ title }) => {const chartRef = useRef(null)useEffect(() => {// 保证dom可用 才进行图表的渲染// 1. 获取渲染图表的dom节点const chartDom = chartRef.current// 2. 图表初始化生成图表实例对象const myChart = echarts.init(chartDom)// 3. 准备图表参数const option = {title: {text: title},xAxis: {type: 'category',data: ['Vue', 'React', 'Angular']},yAxis: {type: 'value'},series: [{data: [10, 40, 70],type: 'bar'}]}// 4. 使用图表参数完成图表的渲染option && myChart.setOption(option)}, [title])return <div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>
}export default BarChart

八、拓展 - API 模块封装

现存问题:

当前的接口请求放到了功能实现的位置,没有在固定的模块内维护,后期查找维护困难

解决思路:

把项目中的所有接口按照业务模块以函数的形式统一封装到 apis 模块中

apis/user.js

// 用户相关的所有请求
import { request } from "@/utils"
// 1. 登录请求
export function loginAPI (formData) {return request({url: '/authorizations',method: 'POST',data: formData})
}// 2. 获取用户信息
export function getProfileAPI () {return request({url: '/user/profile',method: 'GET'})
}

store/modules/user.js

import { loginAPI, getProfileAPI } from '@/apis/user'// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {return async (dispatch) => {const res = await loginAPI(loginForm)dispatch(setToken(res.data.token))}
}// 获取个人用户信息异步方法
const fetchUserInfo = () => {return async (dispatch) => {const res = await getProfileAPI()dispatch(setUserInfo(res.data))}
}

九、文章发布

1. 创建并熟悉基础结构

(1)面包屑导航组件 Breadcrumb

(2)表单组件 Form

(3)输入框组件 Input

(4)下拉框组件 Select - Option

(5)按钮组件 Button

2. 准备富文本编辑器

(1)安装 react-quill 富文本编辑器

// react 18
npm i react-quill@2.0.0-beta.2// react 19
npm i react-quill-new --save

(2)导入编辑器组件和配套样式文件

(3)渲染编辑器组件

(4)调整编辑器组件样式

pages/Publish/index.js

import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'const { Option } = Selectconst Publish = () => {return (<div className="publish"><Cardtitle={<Breadcrumb items={[{ title: <Link to={'/'}>首页</Link> },{ title: `${articleId ? '编辑' : '发布'}文章` },]}/>}><FormlabelCol={{ span: 4 }}wrapperCol={{ span: 16 }}initialValues={{ type: 1 }}><Form.Itemlabel="标题"name="title"rules={[{ required: true, message: '请输入文章标题' }]}><Input placeholder="请输入文章标题" style={{ width: 400 }} /></Form.Item><Form.Itemlabel="频道"name="channel_id"rules={[{ required: true, message: '请选择文章频道' }]}><Select placeholder="请选择文章频道" style={{ width: 400 }}><Option value={0}>推荐</Option></Select></Form.Item><Form.Itemlabel="内容"name="content"rules={[{ required: true, message: '请输入文章内容' }]}>{/* 富文本编辑器 */}<ReactQuillclassName="publish-quill"theme="snow"placeholder="请输入文章内容"/></Form.Item><Form.Item wrapperCol={{ offset: 4 }}><Space><Button size="large" type="primary" htmlType="submit">发布文章</Button></Space></Form.Item></Form></Card></div>)
}export default Publish

3. 频道数据获取渲染

(1)根据接口文档在 APIS 模块中封装接口函数

(2)使用 useState 维护数据

(3)在 useEffect 中调用接口获取数据并存入 state

(4)绑定数据到下拉框组件

apis/article.js

// 封装和文章相关的接口函数
import { request } from "@/utils"// 1. 获取频道列表
export function getChannelAPI () {return request({url: '/channels',method: 'GET'})
}// 2. 提交文章表单
export function createArticleAPI (data) {return request({url: '/mp/articles?draft=false',method: 'POST',data})
}

pages/Publish/index.js  

  // 获取频道列表const [channelList, setChannelList] = useState([])useEffect(() => {// 1. 封装函数 在函数体内调用接口const getChannelList = async () => {const res = await getChannelAPI()setChannelList(res.data.channels)}// 2. 调用函数getChannelList()}, [])

pages/Publish/index.js   

<Form.Itemlabel="频道"name="channel_id"rules={[{ required: true, message: '请选择文章频道' }]}
><Select placeholder="请选择文章频道" style={{ width: 400 }}>{/* value属性用户选中之后会自动收集起来作为接口的提交字段 */}{channelList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}</Select>
</Form.Item>

4. 收集表单数据提交表单

(1)使用 Form 组件收集表单数据

(2)按照接口文档封装接口函数

(3)按照接口文档处理表单数据

(4)提交接口并验证是否成功

pages/Publish/index.js

  // 提交表单const onFinish = (formValue) => {console.log(formValue)const { title, content, channel_id } = formValue// 1. 按照接口文档的格式处理收集到的表单数据const reqData = {title,content,cover: {type: 0,images: [],},channel_id}// 2. 调用接口提交createArticleAPI(reqData)}

5. 上传文章封面基础功能实现

(1)使用现成组件搭建结构

(2)按照 Upload 组件添加配置实现上传

pages/Publish/index.js   

  // 上传回调const [imageList, setImageList] = useState([])const onChange = (value) => {console.log('正在上传中', value)setImageList(value.fileList)}<Form.Item label="封面"><Form.Item name="type"><Radio.Group><Radio value={1}>单图</Radio><Radio value={3}>三图</Radio><Radio value={0}>无图</Radio></Radio.Group></Form.Item>{/* listType: 决定选择文件框的外观样式showUploadList: 控制显示上传列表*/}<UploadlistType="picture-card"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}name='image'onChange={onChange}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload></Form.Item>

6. 实现切换封面类型

实现效果:只有当前模式为单图或者三图模式时才显示上传组件

(1)获取到当前的封面类型

(2)对上传组件进行条件渲染

pages/Publish/index.js   

  // 切换图片封面类型const [imageType, setImageType] = useState(0)const onTypeChange = (e) => {console.log('切换封面了', e.target.value)setImageType(e.target.value)}...<FormlabelCol={{ span: 4 }}wrapperCol={{ span: 16 }}// 控制单选框区域 初始的时候为 0,图片数量为 0initialValues={{ type: 0 }}onFinish={onFinish}form={form}>...<Form.Item label="封面"><Form.Item name="type"><Radio.Group onChange={onTypeChange}><Radio value={1}>单图</Radio><Radio value={3}>三图</Radio><Radio value={0}>无图</Radio></Radio.Group></Form.Item>{/* listType: 决定选择文件框的外观样式showUploadList: 控制显示上传列表*/}{imageType > 0 && <UploadlistType="picture-card"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}name='image'onChange={onChange}maxCount={imageType}fileList={imageList}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>}</Form.Item>...</Form>

7. 控制上传图片的数量

实现的效果:

(1)单图模式时,最多能上传一张图片

(2)三图模式时,最多能上传三张图片

如何实现:

(1)找到限制上传数量的组件属性

(2)使用 imageType 进行绑定控制

pages/Publish/index.js   

            {imageType > 0 && <UploadlistType="picture-card"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}name='image'onChange={onChange}// 控制图片上传数量maxCount={imageType}fileList={imageList}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>}

8. 发布带封面的文章

pages/Publish/index.js   

  // 提交表单const onFinish = (formValue) => {console.log(formValue)// 校验封面类型imageType是否和实际的图片列表imageList数量是相等的if (imageList.length !== imageType) return message.warning('封面类型和图片数量不匹配')const { title, content, channel_id } = formValue// 1. 按照接口文档的格式处理收集到的表单数据const reqData = {title,content,cover: {type: imageType, // 封面模式images: imageList.map(item => item.response.data.url), // 图片列表},channel_id}// 2. 调用接口提交createArticleAPI(reqData)}

十、文章列表模块

1. 功能描述和静态结构创建

 pages/Article/index.js

// 引入汉化包 时间选择器显示中文
import locale from 'antd/es/date-picker/locale/zh_CN'const Article = () => {...return (<div><Cardtitle={<Breadcrumb items={[{ title: <Link to={'/'}>首页</Link> },{ title: '文章列表' },]} />}style={{ marginBottom: 20 }}><Form initialValues={{ status: null }}><Form.Item label="状态" name="status"><Radio.Group><Radio value={''}>全部</Radio><Radio value={1}>待审核</Radio><Radio value={2}>审核通过</Radio></Radio.Group></Form.Item><Form.Item label="频道" name="channel_id"><Selectplaceholder="请选择文章频道"style={{ width: 120 }}><Option value="jack">jack</Option><Option value="jack">jack</Option></Select></Form.Item><Form.Item label="日期" name="date">{/* 传入locale属性 控制中文显示*/}<RangePicker locale={locale}></RangePicker></Form.Item><Form.Item><Button type="primary" htmlType="submit" style={{ marginLeft: 40 }}>筛选</Button></Form.Item></Form></Card>{/* 表格区域 */}<Card title={`根据筛选条件共查询到 ${count} 条结果:`}><Table rowKey="id" columns={columns} dataSource={data} /></Card></div>)
}export default Article

2. 渲染频道数据

在文章管理里面和创建文章里面都要使用 channelList,把这段代码封装成 hook

使用自定义 hook

(1)创建一个 use 打头的函数

(2)在函数中封装业务逻辑,并 return 出组件中要用到的状态数据

(3)组件中导入函数执行并结构状态数据使用

hooks/useChannel.js

// 封装获取频道列表的逻辑
import { useState, useEffect } from 'react'
import { getChannelAPI } from '@/apis/article'
function useChannel () {// 1. 获取频道列表所有的逻辑// 获取频道列表const [channelList, setChannelList] = useState([])useEffect(() => {// 1. 封装函数 在函数体内调用接口const getChannelList = async () => {const res = await getChannelAPI()setChannelList(res.data.channels)}// 2. 调用函数getChannelList()}, [])// 2. 把组件中要用到的数据return出去return {channelList}
}export { useChannel }

pages/Publish/index.js   

import { useChannel } from '@/hooks/useChannel'const Publish = () => {// 获取频道列表const { channelList } = useChannel()...
}

 pages/Article/index.js

import { useChannel } from '@/hooks/useChannel'const Article = () => {const { channelList } = useChannel()
...return (<div>...<Form.Item label="频道" name="channel_id"><Selectplaceholder="请选择文章频道"style={{ width: 120 }}>{channelList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}</Select></Form.Item>...</div>)
}export default Article

3. 渲染 table 文章列表

(1)封装请求接口

(2)使用 useState 维护状态数据

(3)使用 useEffect 发送请求

(4)在组件上绑定对应属性完成渲染

apis/articles.js

// 获取文章列表
export function getArticleListAPI (params) {return request({url: "/mp/articles",method: 'GET',params})
}

pages/Article/index.js

const Article = () => {// 获取文章列表const [list, setList] = useState([])const [count, setCount] = useState(0)useEffect(() => {async function getList () {const res = await getArticleListAPI(reqData)setList(res.data.results)setCount(res.data.total_count)}getList()}, [reqData])
...return ({/* 表格区域 */}<Card title={`根据筛选条件共查询到 ${count} 条结果:`}><Table rowKey="id" columns={columns} dataSource={list} /></Card>)
}

4. 适配文章状态

实现效果:根据文章不同状态在状态列显示不同 Tag

实现思路:

(1)如果要适配的状态只有两个 - 三元条件渲染

(2)如果要适配的状态有多个 - 枚举渲染

const Article = () => {const navigate = useNavigate()const { channelList } = useChannel()// 准备列数据// 定义状态枚举const status = {1: <Tag color='warning'>待审核</Tag>,2: <Tag color='success'>审核通过</Tag>,}const columns = [...{title: '状态',dataIndex: 'status',// data - 后端返回的状态status 根据它做条件渲染// data === 1 => 待审核// data === 2 => 审核通过render: data => status[data]},...]

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

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

相关文章

ngx_http_random_index_module 模块概述

一、使用场景 随机内容分发 当同一目录下存放多份等价内容&#xff08;如多张轮播图、不同版本静态页面等&#xff09;时&#xff0c;可通过随机索引实现负载均衡或流量分散。A/B 测试 通过目录请求自动随机分配用户到不同测试组&#xff0c;无需后端逻辑参与。动态“首页”选…

智能权限守护者:基于Python描述符的动态角色控制实现

智能权限守护者:基于Python描述符的动态角色控制实现 引言:当描述符遇见权限管理 在Python的魔法方法体系中,描述符(Descriptor)以其优雅的属性访问控制机制著称。当我们将描述符与RBAC(基于角色的访问控制)模型结合,就能创造出既灵活又安全的动态权限管理系统。本文…

Linux 的 UDP 网络编程 -- 回显服务器,翻译服务器

目录 1. 回显服务器 -- echo server 1.1 相关函数介绍 1.1.1 socket() 1.1.2 bind() 1.1.3 recvfrom() 1.1.4 sendto() 1.1.5 inet_ntoa() 1.1.6 inet_addr() 1.2 Udp 服务端的封装 -- UdpServer.hpp 1.3 服务端代码 -- UdpServer.cc 1.4 客户端代码 -- UdpClient.…

Linux 内核等待机制详解:prepare_to_wait_exclusive 与 TASK_INTERRUPTIBLE

1. prepare_to_wait_exclusive 函数解析 1.1 核心作用 prepare_to_wait_exclusive 是 Linux 内核中用于将进程以独占方式加入等待队列的关键函数,其主要功能包括: 标记独占等待:通过设置 WQ_FLAG_EXCLUSIVE 标志,表明此等待条目是独占的。 安全入队:在自旋锁保护下,将条…

【Android构建系统】了解Soong构建系统

背景介绍 在Android7.0之前&#xff0c;Android使用GNU Make描述和执行build规则。Android7.0引入了Soong构建系统&#xff0c;弥补Make构建系统在Android层面变慢、容易出错、无法扩展且难以测试等缺点。 Soong利用Kati GNU Make克隆工具和Ninja构建系统组件来加速Android的…

信息学奥赛一本通 1539:简单题 | 洛谷 P5057 [CQOI2006] 简单题

【题目链接】 ybt 1539&#xff1a;简单题 洛谷 P5057 [CQOI2006] 简单题 【题目考点】 1. 树状数组 模板题及讲解&#xff1a;洛谷 P3374 【模板】树状数组 【解题思路】 解法1&#xff1a;树状数组 该有01构成数组初值都为0。 某位置的元素被修改奇数次后值为1&#x…

仓颉开发语言入门教程:搭建开发环境

仓颉开发语言作为华为为鸿蒙系统自研的开发语言&#xff0c;虽然才发布不久&#xff0c;但是它承担着极其重要的历史使命。作为鸿蒙开发者&#xff0c;掌握仓颉开发语言将成为不可或缺的技能&#xff0c;今天我们从零开始&#xff0c;为大家分享仓颉语言的开发教程&#xff0c;…

玉米籽粒发育

成熟玉米籽粒的结构 玉米籽粒的组成 成熟的玉米籽粒主要由以下三部分组成&#xff1a; 母体组织&#xff1a;包括种皮、胎座和花梗。种皮由珠被发育而来&#xff0c;起到保护种子的作用&#xff0c;并在种子的休眠和萌发中发挥重要作用。胚&#xff1a;包含根分生组织、茎分…

sherpa-ncnn:音频处理跟不上采集速度 -- 语音转文本大模型

目录 1. 问题报错2. 解决方法 1. 问题报错 报错&#xff1a; An overrun occurred, which means the RTF of the current model on your board is larger than 1. You can use ./bin/sherpa-ncnn to verify that. Please select a smaller model whose RTF is less than 1 fo…

Postman一直打不开的解决办法

Postman 是一款非常流行的开源 API 开发工具&#xff0c;主要用于构建、测试、调试和文档化应用程序接口&#xff08;API&#xff09;。但有时它的性能不会特别稳定&#xff0c;功能限制和扩展性不足&#xff1b;应用于开发、测试、运维等环节&#xff0c;尤其在开发 RESTful A…

问题|对只允许输入的变量是否进行了更改

“对只允许输入的变量是否进行了更改”这一问题的核心是&#xff1a;在编程中&#xff0c;某些变量被设计为仅用于输入&#xff08;只读&#xff09;&#xff0c;但在代码中可能被意外修改&#xff0c;导致潜在错误。以下是详细解释&#xff1a; 1. 什么是“只允许输入的变量”…

RPC与SOAP的区别

一.RPC&#xff08;远程过程调用&#xff09;和SOAP&#xff08;简单对象访问协议&#xff09;均用于实现分布式系统中的远程通信&#xff0c;但两者在设计理念、协议实现及应用场景上存在显著差异。 二.对比 1.设计理念 2.协议规范 3.技术特性 4.典型应用场景 5.总结 三.总结…

c#的内存指针操作(仅用于记录)

c#也可以直接操作内存指针&#xff0c;如下为示例&#xff1a; unsafe {byte[] a {1,2,3};fixed (byte* p1 a, p2 &a[^1]){Debugger.Log(1, "test", $"max index:{p2-p1}");Debugger.Log(1, "test", $"address:{(long)p1:X}")…

Jsp技术入门指南【十三】基于 JSTL SQL 标签库实现 MySQL 数据库连接与数据分页展示

Jsp技术入门指南【十三】基于 JSTL SQL 标签库实现 MySQL 数据库连接与数据分页展示 前言一、回顾SQL标签的内容1. 什么是JSTL SQL标签&#xff1f;2.为什么要用SQL标签&#xff1f;3.第一步&#xff1a;引入SQL标签库4. SQL标签的核心功能&#xff1a;连接数据库标签常用属性&…

羽毛球订场小程序源码介绍

基于ThinkPHP、FastAdmin以及UniApp开发的羽毛球订场小程序源码&#xff0c;这款小程序旨在为羽毛球爱好者提供便捷的场地预订服务。 该小程序前端采用UniApp框架开发&#xff0c;具有良好的跨平台兼容性&#xff0c;可以一键发布至iOS和Android平台&#xff0c;极大地提高了开…

Unreal Engine: Windows 下打包 AirSim项目 为 Linux 平台项目

环境&#xff1a; Windows: win10, UE4.27, Visual Studio 2022 Community.Linux: 22.04 windows环境安装教程&#xff1a; 链接遇到的问题&#xff08;问题&#xff1a;解决方案&#xff09; 点击Linux打包按钮&#xff0c;跳转至网页而不是执行打包流程&#xff1a;用VS打开项…

SpringBoot 3.x 集成 MyBatisPlus

文章目录 集成 MyBatisPlus第 1 步:创建 SpringBoot 项目第 2 步:添加 MyBatisPlus 依赖第 3 步:编写 CRUD 代码创建 Entity创建 Mapper创建 Service编写 Controller第 4 步:执行初始化 SQL第 5 步:配置第 6 步:测试测试 ControllerMapper 层单元测试参考🚀 目标 1:基…

java基础-抽象类和抽象方法

1.abstract 可以修饰&#xff1a;类、方法 &#xff08;1&#xff09;修饰类&#xff1a; 类不能被实例化&#xff1b; 抽象类一定有构造器&#xff0c;便于子类实例化时调用&#xff1b; &#xff08;2&#xff09;修饰方法&#xff1a;抽象方法 只有方法的声明&#xff…

解决电脑问题(8)——网络问题

电脑网络出现问题的原因较为复杂&#xff0c;以下是从网络连接、网络配置以及网络环境等方面的常见问题及解决方法&#xff1a; 网络连接问题 检查物理连接&#xff1a;对于有线网络&#xff0c;检查网线是否插好&#xff0c;网线有无破损、断裂等情况。对于无线网络&#xff…

ubuntu 20.04 ping baidu.coom可以通,ping www.baidu.com不通 【DNS出现问题】解决方案

ping baidu.coom可以通&#xff0c;ping www.baidu.com不通【DNS出现问题】解决方案 检查IPV6是否有问题 # 1. 检查 IPv6 地址&#xff0c;记住网络接口的名称 ip -6 addr show# 2. 测试本地 IPv6&#xff0c;eth0换成自己的网络接口名称 ping6 ff02::1%eth0# 3. 检查路由 ip…