1.环境搭建以及初始化目录
CRA是一个底层基于webpack快速创建React项目的脚手架工具
# 使用npx创建项目
npx create-react-app react-jike
# 进入到项
cd react-jike
# 启动项目
npm start
2.安装SCSS
SASS
是一种预编译的 CSS,支持一些比较高级的语法,可以提高编写样式的效率,CRA接入scss非常简单只需要我们装一个sass工具
- 安装解析 sass 的包:
npm i sass -D
- 创建全局样式文件:
index.scss
3.安装Ant Design
npm install antd --save
4.配置基础路由Router
npm i react-router-dom
pages/Layout/index.js
const Layout = () => {
return <div>this is layout</div>
}
export default Layout
pages/Login/index.js
const Login = () => {
return <div>this is login</div>
}
export default Login
router/index.js
import { createBrowserRouter } from 'react-router-dom'
import Login from '../pages/Login'
import Layout from '../pages/Layout'
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
},
{
path: '/login',
element: <Login />,
},
])
export default router
index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.scss'
import router from './router'
import { RouterProvider } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')).render(
<RouterProvider router={router} />
)
5.配置别名路径@
- 安装
craco
工具包 - 增加
craco.config.js
配置文件 - 修改
scripts 命令
- 测试是否生效
npm i @craco/craco -D
const path = require('path')
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src')
}
}
}
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
}
import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
},
{
path: '/login',
element: <Login />,
},
])
export default router
VsCode提示配置
实现步骤
- 在项目根目录创建
jsconfig.json
配置文件 - 在配置文件中添加以下配置
代码实现
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
:::warning
说明:VSCode会自动读取jsconfig.json
中的配置,让vscode知道@就是src目录
:::
6.gitee管理项目
git remote add origin http
git add .
git commit -m 'init'
git push
7.登陆模块开发
**实现步骤**
1. 在 `Login/index.js` 中创建登录页面基本结构
2. 在 Login 目录中创建 index.scss 文件,指定组件样式
3. 将 `logo.png` 和 `login.png` 拷贝到 assets 目录中
代码实现
pages/Login/index.js
import './index.scss'
import { Card, Form, Input, Button } from 'antd'
import logo from '@/assets/logo.png'
const Login = () => {
return (
<div className="login">
<Card className="login-container">
<img className="login-logo" src={logo} alt="" />
{/* 登录表单 */}
<Form>
<Form.Item>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
<Form.Item>
<Input size="large" placeholder="请输入验证码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Login
pages/Login/index.scss
.login {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: center/cover url('~@/assets/login.png');
.login-logo {
width: 200px;
height: 60px;
display: block;
margin: 0 auto 20px;
}
.login-container {
width: 440px;
height: 360px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 50px rgb(0 0 0 / 10%);
}
.login-checkbox-label {
color: #1890ff;
}
}
表单校验实现
**实现步骤**
1. 为 Form 组件添加 `validateTrigger` 属性,指定校验触发时机的集合
2. **为 Form.Item 组件添加 name 属性**
3. 为 Form.Item 组件添加 `rules` 属性,用来添加表单校验规则对象
代码实现
page/Login/index.js
const Login = () => {
return (
<Form validateTrigger={['onBlur']}>
<Form.Item
name="mobile"
rules={[
{ required: true, message: '请输入手机号' },
{
pattern: /^1[3-9]\d{9}$/,
message: '手机号码格式不对'
}
]}
>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
<Form.Item
name="code"
rules={[
{ required: true, message: '请输入验证码' },
]}
>
<Input size="large" placeholder="请输入验证码" maxLength={6} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
)
}
获取登录表单数据
**实现步骤**
1. 为 Form 组件添加 `onFinish` 属性,该事件会在点击登录按钮时触发
2. 创建 onFinish 函数,通过函数参数 values 拿到表单值
3. Form 组件添加 `initialValues` 属性,来初始化表单值
代码实现
pages/Login/index.js
// 点击登录按钮时触发 参数values即是表单输入数据
const onFinish = formValue => {
console.log(formValue)
}
<Form
onFinish={ onFinish }
>...</Form>
封装request工具模块
业务背景: 前端需要和后端拉取接口数据,axios是使用最广的工具插件,针对于项目中的使用,我们需要做一些简单的封装
**实现步骤**
1. 安装 axios 到项目
2. 创建 utils/request.js 文件
3. 创建 axios 实例,配置 `baseURL,请求拦截器,响应拦截器`
4. 在 utils/index.js 中,统一导出request
npm i axios
import axios from 'axios'
const http = axios.create({
baseURL: 'http://geek.itheima.net/v1_0',
timeout: 5000
})
// 添加请求拦截器
http.interceptors.request.use((config)=> {
return config
}, (error)=> {
return Promise.reject(error)
})
// 添加响应拦截器
http.interceptors.response.use((response)=> {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error)=> {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
export { http }
import { request } from './request'
export { request }
使用Redux管理token
npm i react-redux @reduxjs/toolkit //安装Redux相关工具包
配置Redux
import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token:''
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.userInfo = action.payload
}
}
})
// 解构出actionCreater
const { setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await http.post('/authorizations', loginForm)
dispatch(setUserInfo(res.data.token))
}
}
export { fetchLogin }
export default userReducer
store下index.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({
reducer: {
// 注册子模块
user: userReducer
}
})
实现登录逻辑
业务逻辑:
- 跳转到首页
- 提示用户登录成功
import { message } from 'antd'
import useStore from '@/store'
import { fetchLogin } from '@/store/modules/user'
import { useDispatch } from 'react-redux'
const Login = () => {
const dispatch = useDispatch()
const navigate = useNavigate()
const onFinish = async formValue => {
await dispatch(fetchLogin(formValue))
navigate('/')
message.success('登录成功')
}
return (
<div className="login">
<!-- 省略... -->
</div>
)
}
export default Login
token持久化
业务背景: Token数据具有一定的时效时间,通常在几个小时,有效时间内无需重新获取,而基于Redux的存储方式又是基于内存的,刷新就会丢失,为了保持持久化,我们需要单独做处理
封装存取方法
// 封装存取方法
const TOKENKEY = 'token_key'
function setToken (token) {
return localStorage.setItem(TOKENKEY, token)
}
function getToken () {
return localStorage.getItem(TOKENKEY)
}
function clearToken () {
return localStorage.removeItem(TOKENKEY)
}
export {
setToken,
getToken,
clearToken
}
实现持久化逻辑
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
},
clearUserInfo (state) {
state.token = ''
state.userInfo = {}
removeToken()
}
}
})
// 解构出actionCreater
const { setToken, setUserInfo, clearUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 登录获取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))
}
}
export { fetchLogin, fetchUserInfo, clearUserInfo }
export default userReducer
刷新浏览器,通过Redux调试工具查看token数据
请求拦截器注入token
业务背景: Token作为用户的数据标识,在接口层面起到了接口权限控制的作用,也就是说后端有很多接口都需要通过查看当前请求头信息中是否含有token数据,来决定是否正常返回数据
拼接方式:config.headers.Authorization =
Bearer ${token}}
utils/request.js
// 添加请求拦截器
request.interceptors.request.use(config => {
// if not login add token
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
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)
})
export { request }
路由鉴权实现
业务背景:封装
AuthRoute
路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面
实现思路:判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login
**实现步骤**
1. 在 components 目录中,创建 `AuthRoute/index.jsx` 文件
2. 登录时,直接渲染相应页面组件
3. 未登录时,重定向到登录页面
4. 将需要鉴权的页面路由配置,替换为 AuthRoute 组件渲染
代码实现
components/AuthRoute/index.jsx
import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'
const AuthRoute = ({ children }) => {
const isToken = getToken()
if (isToken) {
return <>{children}</>
} else {
return <Navigate to="/login" replace />
}
}
export default AuthRoute
src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import AuthRoute from '@/components/Auth'
const router = createBrowserRouter([
{
path: '/',
element: <AuthRoute><Layout /></AuthRoute>,
},
{
path: '/login',
element: <Login />,
},
])
export default router
9.layout模块
基本结构和样式reset
实现步骤
- 打开
antd/Layout
布局组件文档,找到示例:顶部-侧边布局-通栏 - 拷贝示例代码到我们的 Layout 页面中
- 分析并调整页面布局
代码实现
pages/Layout/index.js
import { Layout, Menu, Popconfirm } from 'antd'
import {
HomeOutlined,
DiffOutlined,
EditOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import './index.scss'
const { Header, Sider } = Layout
const items = [
{
label: '首页',
key: '1',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '2',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '3',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
return (
<Layout>
<Header className="header">
<div className="logo" />
<div className="user-info">
<span className="user-name">柴柴老师</span>
<span className="user-logout">
<Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
<LogoutOutlined /> 退出
</Popconfirm>
</span>
</div>
</Header>
<Layout>
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
defaultSelectedKeys={['1']}
items={items}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
内容
</Layout>
</Layout>
</Layout>
)
}
export default GeekLayout
pages/Layout/index.scss
.ant-layout {
height: 100%;
}
.header {
padding: 0;
}
.logo {
width: 200px;
height: 60px;
background: url('~@/assets/logo.png') no-repeat center / 160px auto;
}
.layout-content {
overflow-y: auto;
}
.user-info {
position: absolute;
right: 0;
top: 0;
padding-right: 20px;
color: #fff;
.user-name {
margin-right: 20px;
}
.user-logout {
display: inline-block;
cursor: pointer;
}
}
.ant-layout-header {
padding: 0 !important;
}
样式reset
npm install normalize.css
html,
body {
margin: 0;
height: 100%;
}
#root {
height: 100%;
}
二级路由配置
使用步骤
- 在 pages 目录中,分别创建:Home(数据概览)/Article(内容管理)/Publish(发布文章)页面文件夹
- 分别在三个文件夹中创建 index.jsx 并创建基础组件后导出
- 在
router/index.js
中配置嵌套子路由,在Layout
中配置二级路由出口 - 使用 Link 修改左侧菜单内容,与子路由规则匹配实现路由切换
代码实现
pages/Home/index.js
const Home = () => {
return <div>Home</div>
}
export default Home
pages/Article/index.js
const Article = () => {
return <div>Article</div>
}
export default Article
pages/Publish/index.js
const Publish = () => {
return <div>Publish</div>
}
export default Publish
router/index.js
import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import Publish from '@/pages/Publish'
import Article from '@/pages/Article'
import Home from '@/pages/Home'
import { AuthRoute } from '@/components/Auth'
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
配置二级路由出口
<Layout className="layout-content" style={{ padding: 20 }}>
<Outlet />
</Layout>
路由菜单点击交互实现
点击菜单跳转路由
import { Outlet, useNavigate } from 'react-router-dom'
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
const navigate = useNavigate()
const menuClick = (route) => {
navigate(route.key)
}
return (
<Menu
mode="inline"
theme="dark"
selectedKeys={selectedKey}
items={items}
style={{ height: '100%', borderRight: 0 }}
onClick={menuClick}
/>
)
}
export default GeekLayout
菜单反向高亮
const GeekLayout = () => {
// 省略部分代码
const location = useLocation()
const selectedKey = location.pathname
return (
<Layout>
<Header className="header">
<div className="logo" />
<div className="user-info">
<span className="user-name">{name}</span>
<span className="user-logout">
<Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
<LogoutOutlined /> 退出
</Popconfirm>
</span>
</div>
</Header>
<Layout>
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
selectedKeys={selectedKey}
items={items}
style={{ height: '100%', borderRight: 0 }}
onClick={menuClickHandler}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
展示个人信息
实现步骤
- 在Redux的store中编写获取用户信息的相关逻辑
- 在Layout组件中触发action的执行
- 在Layout组件使用使用store中的数据进行用户名的渲染
代码实现
store/userStore.js
import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils/request'
import { getToken, setToken } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据
initialState: {
token: getToken() || '',
userInfo: {}
},
// 同步修改方法
reducers: {
setUserToken (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
},
setUserInfo (state, action) {
state.userInfo = action.payload
}
}
})
// 解构出actionCreater
const { setUserToken, setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await http.post('/authorizations', loginForm)
dispatch(setUserToken(res.data.token))
}
}
const fetchUserInfo = () => {
return async (dispatch) => {
const res = await http.get('/user/profile')
dispatch(setUserInfo(res.data))
}
}
export { fetchLogin, fetchUserInfo }
export default userReducer
pages/Layout/index.js
// 省略部分代码
import { fetchUserInfo } from '@/store/modules/user'
import { useDispatch, useSelector } from 'react-redux'
const GeekLayout = () => {
const dispatch = useDispatch()
const name = useSelector(state => state.user.userInfo.name)
useEffect(() => {
dispatch(fetchUserInfo())
}, [dispatch])
return (
<Layout>
<Header className="header">
<div className="logo" />
<div className="user-info">
<span className="user-name">{name}</span>
<span className="user-logout">
<Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
<LogoutOutlined /> 退出
</Popconfirm>
</span>
</div>
</Header>
<Layout>
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
defaultSelectedKeys={['1']}
items={items}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
export default GeekLayout
退出登录实现
实现步骤
- 为气泡确认框添加确认回调事件
- 在
store/userStore.js
中新增退出登录的action函数,在其中删除token - 在回调事件中,调用userStore中的退出action
- 清除用户信息,返回登录页面
代码实现
store/modules/user.js
import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils/request'
import { clearToken, getToken, setToken } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据
initialState: {
token: getToken() || '',
userInfo: {}
},
// 同步修改方法
reducers: {
setUserToken (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
},
setUserInfo (state, action) {
state.userInfo = action.payload
},
clearUserInfo (state) {
state.token = ''
state.userInfo = {}
clearToken()
}
}
})
// 解构出actionCreater
const { setUserToken, setUserInfo, clearUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
export { fetchLogin, fetchUserInfo, clearUserInfo }
export default userReducer
pages/Layout/index.js
const GeekLayout = () => {
// 退出登录
const loginOut = () => {
dispatch(clearUserInfo())
navigator('/login')
}
return (
<Layout>
<Header className="header">
<div className="logo" />
<div className="user-info">
<span className="user-name">{name}</span>
<span className="user-logout">
<Popconfirm
title="是否确认退出?"
okText="退出"
cancelText="取消"
onConfirm={loginOut}>
<LogoutOutlined /> 退出
</Popconfirm>
</span>
</div>
</Header>
<Layout>
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
selectedKeys={selectedKey}
items={items}
style={{ height: '100%', borderRight: 0 }}
onClick={menuClickHandler}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
处理Token失效
业务背景:如果用户一段时间不做任何操作,到时之后应该清除所有过期用户信息跳回到登录
http.interceptors.response.use((response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
console.dir(error)
if (error.response.status === 401) {
clearToken()
router.navigate('/login')
window.location.reload()
}
return Promise.reject(error)
})
首页Home图表展示
图表基础Demo实现
图表类业务渲染,我们可以通过下面的顺序来实现
- 跑通基础DEMO
- 按照实际业务需求进行修改
安装echarts
npm i echarts
实现基础Demo
import { useEffect, useRef } from 'react'
import * as echarts from 'echarts'
const Home = () => {
const chartRef = useRef(null)
useEffect(() => {
// 1. 生成实例
const myChart = echarts.init(chartRef.current)
// 2. 准备图表参数
const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar'
}
]
}
// 3. 渲染参数
myChart.setOption(option)
}, [])
return (
<div>
<div ref={chartRef} style={{ width: '400px', height: '300px' }} />
</div >
)
}
export default Home
组件封装
基础抽象
import { useRef, useEffect } from 'react'
import * as echarts from 'echarts'
const BarChart = () => {
const chartRef = useRef(null)
useEffect(() => {
// 1. 生成实例
const myChart = echarts.init(chartRef.current)
// 2. 准备图表参数
const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar'
}
]
}
// 3. 渲染参数
myChart.setOption(option)
}, [])
return <div ref={chartRef} style={{ width: '400px', height: '300px' }}></div>
}
export { BarChart }
抽象可变参数
import { useRef, useEffect } from 'react'
import * as echarts from 'echarts'
const BarChart = ({ xData, sData, style = { width: '400px', height: '300px' } }) => {
const chartRef = useRef(null)
useEffect(() => {
// 1. 生成实例
const myChart = echarts.init(chartRef.current)
// 2. 准备图表参数
const option = {
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
series: [
{
data: sData,
type: 'bar'
}
]
}
// 3. 渲染参数
myChart.setOption(option)
}, [sData, xData])
return <div ref={chartRef} style={style}></div>
}
export { BarChart }
import { BarChart } from './BarChart'
const Home = () => {
return (
<div>
<BarChart
xData={['Vue', 'React', 'Angular']}
sData={[2000, 5000, 1000]} />
<BarChart
xData={['Vue', 'React', 'Angular']}
sData={[200, 500, 100]}
style={{ width: '500px', height: '400px' }} />
</div >
)
}
export default Home
10.API模块封装
// 用户相关的所有请求
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'
})
}
11.发布文章模块
实现基础文章发布
创建基础结构
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Input,
Upload,
Space,
Select
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'
import './index.scss'
const { Option } = Select
const Publish = () => {
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '发布文章' },
]}
/>
}
>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 1 }}
>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入文章标题' }]}
>
<Input placeholder="请输入文章标题" style={{ width: 400 }} />
</Form.Item>
<Form.Item
label="频道"
name="channel_id"
rules={[{ required: true, message: '请选择文章频道' }]}
>
<Select placeholder="请选择文章频道" style={{ width: 400 }}>
<Option value={0}>推荐</Option>
</Select>
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
></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
pages/Publish/index.scss
.publish {
position: relative;
}
.ant-upload-list {
.ant-upload-list-picture-card-container,
.ant-upload-select {
width: 146px;
height: 146px;
}
}
准备富文本编辑器
**实现步骤**
1. 安装富文本编辑器
2. 导入富文本编辑器组件以及样式文件
3. 渲染富文本编辑器组件
4. 调整富文本编辑器的样式
代码落地
1-安装 react-quill
npm i react-quill@2.0.0-beta.2
2-导入资源渲染组件
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
const Publish = () => {
return (
// ...
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="publish-quill"
theme="snow"
placeholder="请输入文章内容"
/>
</Form.Item>
</Form>
)
}
.publish-quill {
.ql-editor {
min-height: 300px;
}
}
频道数据获取
实现步骤
- 使用useState初始化数据和修改数据的方法
- 在useEffect中调用接口并保存数据
- 使用数据渲染对应模版
代码实现
import { http } from '@/utils'
// 频道列表
const [channels, setChannels] = useState([])
// 调用接口
useEffect(() => {
async function fetchChannels() {
const res = await http.get('/channels')
setChannels(res.data.channels)
}
fetchChannels()
}, [])
// 模板渲染
return (
<Form.Item
label="频道"
name="channel_id"
rules={[{ required: true, message: '请选择文章频道' }]}
>
<Select placeholder="请选择文章频道" style={{ width: 200 }}>
{channels.map(item => (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
))}
</Select>
</Form.Item>
)
发布文章
// 发布文章
const onFinish = async (formValue) => {
const { channel_id, content, title } = formValue
const params = {
channel_id,
content,
title,
type: 1,
cover: {
type: 1,
images: []
}
}
await http.post('/mp/articles?draft=false', params)
message.success('发布文章成功')
}
上传封面实现
准备上传结构
<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>
<Upload
listType="picture-card"
showUploadList
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>
</Form.Item>
实现基础上传
实现步骤
- 为 Upload 组件添加
action 属性
,配置封面图片上传接口地址 - 为 Upload组件添加
name属性
, 接口要求的字段名 - 为 Upload 添加
onChange 属性
,在事件中拿到当前图片数据,并存储到React状态中
代码实现
import { useState } from 'react'
const Publish = () => {
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
}
return (
<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>
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>
</Form.Item>
)
}
切换图片Type
**实现步骤**
1. 点击单选框时拿到当前的类型value
2. 根据value控制上传组件的显示(大于零时才显示)
const Publish = ()=>{
// 控制图片Type
const [imageType, setImageType] = useState(0)
const onTypeChange = (e) => {
console.log(e)
setImageType(e.target.value)
}
return (
<FormItem>
<Radio.Group onChange={onTypeChange}>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
{imageType > 0 &&
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</FormItem>
)
}
控制最大上传图片数量
实现步骤
- 通过 maxCount 属性限制图片的上传图片数量
{imageType > 0 &&
<Upload
name="image"
listType="picture-card"
className="avatar-uploader"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={imageType}
multiple={imageType > 1}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
暂存图片列表实现
业务描述
如果当前为三图模式,已经完成了上传,选择单图只显示一张,再切换到三图继续显示三张,该如何实现?
实现思路
在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实也就是把ref当仓库,用多少拿多少
实现步骤
- 通过useRef创建一个暂存仓库,在上传完毕图片的时候把图片列表存入
- 如果是单图模式,就从仓库里取第一张图,以数组的形式存入fileList
- 如果是三图模式,就把仓库里所有的图片,以数组的形式存入fileList
代码实现
const Publish = () => {
// 上传图片
const cacheImageList = useRef([])
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
cacheImageList.current = info.fileList
}
// 控制图片Type
const [imageType, setImageType] = useState(0)
const onRadioChange = (e) => {
const type = e.target.value
setImageType(type)
if (type === 1) {
// 单图,截取第一张展示
const imgList = cacheImageList.current[0] ? [cacheImageList.current[0]] : []
setImageList(imgList)
} else if (type === 3) {
// 三图,取所有图片展示
setImageList(cacheImageList.current)
}
}
return (
{imageType > 0 &&
<Upload
name="image"
listType="picture-card"
className="avatar-uploader"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={imageType}
multiple={imageType > 1}
fileList={imageList}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
)
}
注意:需要给Upload组件添加fileList属性,达成受控的目的
发布带封面的文章
校验图片类型和数量是否吻合
// 发布文章
const onFinish = async (formValue) => {
if (imageType !== imageList.length) return message.warning('图片类型和数量不一致')
const { channel_id, content, title } = formValue
const params = {
channel_id,
content,
title,
type: imageType,
cover: {
type: imageType,
images: imageList.map(item => item.response.data.url)
}
}
await http.post('/mp/articles?draft=false', params)
message.success('发布文章成功')
}
12.文章列表模块
静态结构创建
筛选区结构搭建
- 如何让RangePicker日期范围选择框选择中文
- Select组件配合Form.Item使用时,如何配置默认选中项
<Form initialValues={{ status: null }} >
代码实现
import { Link } from 'react-router-dom'
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd'
import locale from 'antd/es/date-picker/locale/zh_CN'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
return (
<div>
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '文章列表' },
]} />
}
style={{ marginBottom: 20 }}
>
<Form initialValues={{ status: '' }}>
<Form.Item label="状态" name="status">
<Radio.Group>
<Radio value={''}>全部</Radio>
<Radio value={0}>草稿</Radio>
<Radio value={2}>审核通过</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="频道" name="channel_id">
<Select
placeholder="请选择文章频道"
defaultValue="lucy"
style={{ width: 120 }}
>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</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>
</div>
)
}
export default Article
表格区域结构
代码实现
// 导入资源
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import img404 from '@/assets/error.png'
const Article = () => {
// 准备列数据
const columns = [
{
title: '封面',
dataIndex: 'cover',
width: 120,
render: cover => {
return <img src={cover.images[0] || img404} width={80} height={60} alt="" />
}
},
{
title: '标题',
dataIndex: 'title',
width: 220
},
{
title: '状态',
dataIndex: 'status',
render: data => <Tag color="green">审核通过</Tag>
},
{
title: '发布时间',
dataIndex: 'pubdate'
},
{
title: '阅读数',
dataIndex: 'read_count'
},
{
title: '评论数',
dataIndex: 'comment_count'
},
{
title: '点赞数',
dataIndex: 'like_count'
},
{
title: '操作',
render: data => {
return (
<Space size="middle">
<Button type="primary" shape="circle" icon={<EditOutlined />} />
<Button
type="primary"
danger
shape="circle"
icon={<DeleteOutlined />}
/>
</Space>
)
}
}
]
// 准备表格body数据
const data = [
{
id: '8218',
comment_count: 0,
cover: {
images: [],
},
like_count: 0,
pubdate: '2019-03-11 09:00:00',
read_count: 2,
status: 2,
title: 'wkwebview离线化加载h5资源解决方案'
}
]
return (
<div>
{/* */}
<Card title={`根据筛选条件共查询到 count 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={data} />
</Card>
</div>
)
}
渲染频道数据
实现步骤
- 使用axios获取数据
- 将使用频道数据列表改写下拉框组件
代码实现
pages/Article/index.js
const Article = ()=>{
// 获取频道列表
const [channels, setChannels] = useState([])
useEffect(() => {
async function fetchChannels() {
const res = await http.get('/channels')
setChannels(res.data.channels)
}
fetchChannels()
}, [])
// 渲染模板
return (
<Form.Item label="频道" name="channel_id" >
<Select placeholder="请选择频道" style={{ width: 200 }} >
{channels.map(item => (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
))}
</Select>
</Form.Item>
)
}
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/Article/index.js
import { useChannel } from '@/hooks/useChannel'
const { channelList } = useChannel()
<Form.Item label="频道" name="channel_id">
<Select placeholder="请选择文章频道" style={{ width: 120 }}>
{channelList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}</Select>
</Form.Item>
渲染表格数据
实现步骤
- 声明列表相关数据管理
- 使用useState声明参数相关数据管理
- 调用接口获取数据
- 使用接口数据渲染模板
代码实现
const Article = ()=>{
// 省略部分代码...
// 文章列表数据管理
const [article, setArticleList] = useState({
list: [],
count: 0
})
const [params, setParams] = useState({
page: 1,
per_page: 4,
begin_pubdate: null,
end_pubdate: null,
status: null,
channel_id: null
})
useEffect(() => {
async function fetchArticleList () {
const res = await http.get('/mp/articles', { params })
const { results, total_count } = res.data
setArticleList({
list: results,
count: total_count
})
}
fetchArticleList()
}, [params])
// 模板渲染
return (
<Card title={`根据筛选条件共查询到 ${article.count} 条结果:`}>
<Table
dataSource={article.list}
columns={columns}
/>
</Card>
)
}
筛选功能实现
实现步骤
- 为表单添加
onFinish
属性监听表单提交事件,获取参数 - 根据接口字段格式要求格式化参数格式
- 修改
params
参数并重新使用新参数重新请求数据
代码实现
// 获取文章列表
const [list, setList] = useState([])
const [count, setCount] = useState(0)
async function getList (reqData = {}) {
const res = await getArticleListAPI(reqData)
setList(res.data.results)
setCount(res.data.total_count)
}
useEffect(() => {
getList()
}, [])
// 筛选文章列表
const onFinish = async (formValue) => {
console.log(formValue)
// 1. 准备参数
const { channel_id, date, status } = formValue
const reqData = {
status,
channel_id,
begin_pubdate: date[0].format('YYYY-MM-DD'),
end_pubdate: date[1].format('YYYY-MM-DD'),
}
// 2. 使用参数获取新的列表
getList(reqData)
}
分页功能实现
实现步骤
- 为Table组件指定pagination属性来展示分页效果
- 在分页切换事件中获取到筛选表单中选中的数据
- 使用当前页数据修改params参数依赖引起接口重新调用获取最新数据
代码实现
const pageChange = (page) => {
// 拿到当前页参数 修改params 引起接口更新
setParams({
...params,
page
})
}
return (
<Table rowKey="id" columns={columns} dataSource={article.list} pagination={{
current: params.page,
pageSize: params.per_page,
onChange: pageChange,
total: article.count
}} />
)
删除功能
实现步骤
- 给删除文章按钮绑定点击事件
- 弹出确认窗口,询问用户是否确定删除文章
- 拿到参数调用删除接口,更新列表
代码实现
// 删除回调
const delArticle = async (data) => {
await http.delete(`/mp/articles/${data.id}`)
// 更新列表
setParams({
page: 1,
per_page: 10
})
}
const columns = [
// ...
{
title: '操作',
render: data => {
return (
<Space size="middle">
<Button type="primary" shape="circle" icon={<EditOutlined />} />
<Popconfirm
title="确认删除该条文章吗?"
onConfirm={() => delArticle(data)}
okText="确认"
cancelText="取消"
>
<Button
type="primary"
danger
shape="circle"
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space>
)
}
]
编辑文章跳转
代码实现
const columns = [
// ...
{
title: '操作',
render: data => (
<Space size="middle">
<Button
type="primary"
shape="circle"
icon={<EditOutlined />}
onClick={() => navagite(`/publish?id=${data.id}`)} />
/>
</Space>
)
}
]