当前位置: 首页 > news >正文

react+AntDesign 之 pc端项目案例

1.环境搭建以及初始化目录

CRA是一个底层基于webpack快速创建React项目的脚手架工具
在这里插入图片描述

# 使用npx创建项目
npx create-react-app react-jike# 进入到项
cd react-jike# 启动项目
npm start

2.安装SCSS

SASS 是一种预编译的 CSS,支持一些比较高级的语法,可以提高编写样式的效率,CRA接入scss非常简单只需要我们装一个sass工具

  1. 安装解析 sass 的包:npm i sass -D
  2. 创建全局样式文件: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.配置别名路径@

  1. 安装 craco 工具包
  2. 增加 craco.config.js 配置文件
  3. 修改 scripts 命令
  4. 测试是否生效
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提示配置

实现步骤

  1. 在项目根目录创建 jsconfig.json 配置文件
  2. 在配置文件中添加以下配置

代码实现

{"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.Itemname="mobile"rules={[{ required: true, message: '请输入手机号' },{pattern: /^1[3-9]\d{9}$/,message: '手机号码格式不对'}]}><Input size="large" placeholder="请输入手机号" /></Form.Item><Form.Itemname="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)
}<FormonFinish={ 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}
})

实现登录逻辑

业务逻辑:

  1. 跳转到首页
  2. 提示用户登录成功
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()}}
})// 解构出actionCreaterconst { 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 tokenconst 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

在这里插入图片描述

实现步骤

  1. 打开 antd/Layout 布局组件文档,找到示例:顶部-侧边布局-通栏
  2. 拷贝示例代码到我们的 Layout 页面中
  3. 分析并调整页面布局

代码实现
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 } = Layoutconst 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"><Menumode="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%;
}

二级路由配置

在这里插入图片描述

使用步骤

  1. 在 pages 目录中,分别创建:Home(数据概览)/Article(内容管理)/Publish(发布文章)页面文件夹
  2. 分别在三个文件夹中创建 index.jsx 并创建基础组件后导出
  3. router/index.js 中配置嵌套子路由,在Layout中配置二级路由出口
  4. 使用 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 (<Menumode="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.pathnamereturn (<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"><Menumode="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>)
}

展示个人信息

在这里插入图片描述

实现步骤

  1. 在Redux的store中编写获取用户信息的相关逻辑
  2. 在Layout组件中触发action的执行
  3. 在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.reducerconst 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"><Menumode="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

退出登录实现

实现步骤

  1. 为气泡确认框添加确认回调事件
  2. store/userStore.js 中新增退出登录的action函数,在其中删除token
  3. 在回调事件中,调用userStore中的退出action
  4. 清除用户信息,返回登录页面

代码实现
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.reducerexport { 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"><Menumode="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实现

图表类业务渲染,我们可以通过下面的顺序来实现

  1. 跑通基础DEMO
  2. 按照实际业务需求进行修改

安装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><BarChartxData={['Vue', 'React', 'Angular']}sData={[2000, 5000, 1000]} /><BarChartxData={['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 } = Selectconst Publish = () => {return (<div className="publish"><Cardtitle={<Breadcrumb items={[{ title: <Link to={'/'}>首页</Link> },{ title: '发布文章' },]}/>}><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: '请输入文章内容' }]}></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 (// ...<FormlabelCol={{ span: 4 }}wrapperCol={{ span: 16 }}><Form.Itemlabel="内容"name="content"rules={[{ required: true, message: '请输入文章内容' }]}><ReactQuillclassName="publish-quill"theme="snow"placeholder="请输入文章内容"/></Form.Item></Form>)
}
.publish-quill {.ql-editor {min-height: 300px;}
}

频道数据获取

实现步骤

  1. 使用useState初始化数据和修改数据的方法
  2. 在useEffect中调用接口并保存数据
  3. 使用数据渲染对应模版

代码实现

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.Itemlabel="频道"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 } = formValueconst 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><UploadlistType="picture-card"showUploadList><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>
</Form.Item>

实现基础上传

实现步骤

  1. 为 Upload 组件添加 action 属性,配置封面图片上传接口地址
  2. 为 Upload组件添加 name属性, 接口要求的字段名
  3. 为 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><Uploadname="image"listType="picture-card"showUploadListaction={'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 = ()=>{// 控制图片Typeconst [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 &&<Uploadname="image"listType="picture-card"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}onChange={onUploadChange}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>}</FormItem>)
}

控制最大上传图片数量

实现步骤

  1. 通过 maxCount 属性限制图片的上传图片数量
{imageType > 0 &&
<Uploadname="image"listType="picture-card"className="avatar-uploader"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}onChange={onUploadChange}maxCount={imageType}multiple={imageType > 1}
><div style={{ marginTop: 8 }}><PlusOutlined /></div>
</Upload>}

暂存图片列表实现

业务描述
如果当前为三图模式,已经完成了上传,选择单图只显示一张,再切换到三图继续显示三张,该如何实现?

实现思路
在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实也就是把ref当仓库,用多少拿多少

实现步骤

  1. 通过useRef创建一个暂存仓库,在上传完毕图片的时候把图片列表存入
  2. 如果是单图模式,就从仓库里取第一张图,以数组的形式存入fileList
  3. 如果是三图模式,就把仓库里所有的图片,以数组的形式存入fileList

代码实现

const Publish = () => {// 上传图片const cacheImageList = useRef([])const [imageList, setImageList] = useState([])const onUploadChange = (info) => {setImageList(info.fileList)cacheImageList.current = info.fileList}// 控制图片Typeconst [imageType, setImageType] = useState(0)const onRadioChange = (e) => {const type = e.target.valuesetImageType(type)if (type === 1) {// 单图,截取第一张展示const imgList = cacheImageList.current[0] ? [cacheImageList.current[0]] : []setImageList(imgList)} else if (type === 3) {// 三图,取所有图片展示setImageList(cacheImageList.current)}}return ({imageType > 0 &&<Uploadname="image"listType="picture-card"className="avatar-uploader"showUploadListaction={'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 } = formValueconst 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.文章列表模块

静态结构创建

在这里插入图片描述

筛选区结构搭建

  1. 如何让RangePicker日期范围选择框选择中文
  2. 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 } = DatePickerconst Article = () => {return (<div><Cardtitle={<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"><Selectplaceholder="请选择文章频道"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 />} /><Buttontype="primary"dangershape="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>)
}

渲染频道数据

实现步骤

  1. 使用axios获取数据
  2. 将使用频道数据列表改写下拉框组件

代码实现
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>

渲染表格数据

实现步骤

  1. 声明列表相关数据管理
  2. 使用useState声明参数相关数据管理
  3. 调用接口获取数据
  4. 使用接口数据渲染模板

代码实现

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.datasetArticleList({list: results,count: total_count})}fetchArticleList()}, [params])// 模板渲染return (<Card title={`根据筛选条件共查询到 ${article.count} 条结果:`}><TabledataSource={article.list}columns={columns}/></Card>)
}

筛选功能实现

实现步骤

  1. 为表单添加onFinish属性监听表单提交事件,获取参数
  2. 根据接口字段格式要求格式化参数格式
  3. 修改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 } = formValueconst reqData = {status,channel_id,begin_pubdate: date[0].format('YYYY-MM-DD'),end_pubdate: date[1].format('YYYY-MM-DD'),}// 2. 使用参数获取新的列表getList(reqData)}

分页功能实现

实现步骤

  1. 为Table组件指定pagination属性来展示分页效果
  2. 在分页切换事件中获取到筛选表单中选中的数据
  3. 使用当前页数据修改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}} />
)

删除功能

实现步骤

  1. 给删除文章按钮绑定点击事件
  2. 弹出确认窗口,询问用户是否确定删除文章
  3. 拿到参数调用删除接口,更新列表

代码实现

// 删除回调
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 />} /><Popconfirmtitle="确认删除该条文章吗?"onConfirm={() => delArticle(data)}okText="确认"cancelText="取消"><Buttontype="primary"dangershape="circle"icon={<DeleteOutlined />}/></Popconfirm></Space>)}
]

编辑文章跳转

代码实现

const columns = [// ...{title: '操作',render: data => (<Space size="middle"><Buttontype="primary"shape="circle"icon={<EditOutlined />}onClick={() => navagite(`/publish?id=${data.id}`)} />/></Space>)}
]

相关文章:

  • SpikingJelly笔记之泊松编码
  • Linux备忘手册
  • ML:2-4理解python如何实现forward prop
  • TypeScript使用技巧内置工具类型详解
  • 抛弃安卓,追赶iOS,鸿蒙能否成为中国第二大系统?
  • vue.js环境在window和linux安装
  • springcloud gateway动态路由
  • LeetCode[105] 从前序与中序遍历序列构造二叉树
  • 10+设计素材网站推荐,PSD文件及海量背景素材免费下载!
  • How to build a localized sdkman mirror service
  • [UI5] ODATA V4中的CRUD
  • uniapp 文字超出多少字,显示收起全文按钮效果demo(整理)
  • Mockito+junit5搞定单元测试
  • 中国人民大学与加拿大女王大学金融硕士项目:努力经营当下,以致未来明朗
  • 【unity小技巧】实现没有动画的FPS武器摇摆和摆动效果
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 【刷算法】从上往下打印二叉树
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • Centos6.8 使用rpm安装mysql5.7
  • eclipse(luna)创建web工程
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • HTML5新特性总结
  • JavaScript 基本功--面试宝典
  • Laravel Mix运行时关于es2015报错解决方案
  • Leetcode 27 Remove Element
  • Vue 2.3、2.4 知识点小结
  • 当SetTimeout遇到了字符串
  • 高度不固定时垂直居中
  • 如何胜任知名企业的商业数据分析师?
  • 数据科学 第 3 章 11 字符串处理
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 一文看透浏览器架构
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • ​linux启动进程的方式
  • ​你们这样子,耽误我的工作进度怎么办?
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • #我与Java虚拟机的故事#连载15:完整阅读的第一本技术书籍
  • (06)Hive——正则表达式
  • (floyd+补集) poj 3275
  • (java版)排序算法----【冒泡,选择,插入,希尔,快速排序,归并排序,基数排序】超详细~~
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (转)socket Aio demo
  • .MSSQLSERVER 导入导出 命令集--堪称经典,值得借鉴!
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .NET Core 版本不支持的问题
  • .NET Core 实现 Redis 批量查询指定格式的Key
  • .net Signalr 使用笔记
  • .NetCore部署微服务(二)
  • .NET学习全景图
  • .NET正则基础之——正则委托
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复
  • [《百万宝贝》观后]To be or not to be?
  • [AIGC] Java 和 Kotlin 的区别
  • [Angular] 笔记 16:模板驱动表单 - 选择框与选项
  • [C#]winform使用引导APSF和梯度自适应卷积增强夜间雾图像的可见性算法实现夜间雾霾图像的可见度增强