极客园
初始化项目
CRA创建项目
npx create-react-app react-jike
新建文件夹

删除没有用的文件和代码
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
App.js
function App() {
return (
<div className="App">
this is my app
</div>
);
}
export default App;

安装scss包

npm i sass -D
安装antDesign组件库
官方地址https://ant.design/docs/react/getting-started-cn
ps:因为跟着视频的老版本做的,官方的已经没有这个文档了
npm install antd --save
引用试试
App.js
import { Button } from "antd";
function App() {
return (
<div className="App">
this is my app <Button type="primary">test</Button>
</div>
);
}
export default App;

配置基础路由Router

下载路由包
npm i react-router-dom
新建页面

新建路由文件

import Layout from "../pages/Layout";
import Login from "../pages/Login";
import { createBrowserRouter } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
},
{
path: "/login",
element: <Login />,
},
]);
export default router;
引入路由

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.scss';
import { RouterProvider } from 'react-router-dom';
import router from './router';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);

配置别名路径

安装依赖
npm i @craco/craco -D
修改webpack别名路径配置craco

新增craco.config.js文件

const path = require("path");
module.exports = {
webpack: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
}
替换

配置联想路径
新建文件

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

使用gitee管理项目
在gitee上新建仓库

cd existing_git_repo
git remote add origin https://gitee.com/你自己的/react-jike-2026.git
git push -u origin "master"
在code里使用命令
git remote add origin https://gitee.com/你的/react-jike-2026.git
git add .
git commit -m "init"
git push
git push --set-upstream origin master
成功


ps:一般开发需要上传到dev开发分支
# 创建并切换到 dev 分支
git checkout -b dev
# 推送到远程 dev 分支
git push -u origin dev
git add .
git commit -m "init1"
git push
git push --set-upstream origin dev
#以后可以用这个
git push
以后切换好直接推送即可


登录
准备基础静态结构代码实现
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;
}
}
启动 npm run start
切换路由

表单校验实现
使用的是design组件里的表单组件
https://ant.design/components/form-cn

把校验贴过来并且按需修改

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
name="mobile"
rules={[{ required: true, message: '请填写你的手机号' }]}
>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
<Form.Item
name="code"
rules={[{ required: true, message: '请填写验证码' }]}
>
<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

添加失去聚焦

<Form validateTrigger='onBlur'>

手机号为有效格式

<Form validateTrigger='onBlur'>
<Form.Item
name="mobile"
rules={[
{ required: true, message: '请填写你的手机号' },
{
pattern: /^1[3-9]\d{9}$/,
message: '手机号码格式不对'
}
]}
>
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>
)
}

获取表单数据

import './index.scss'
import { Card, Form, Input, Button } from 'antd'
import logo from '@/assets/logo.png'
const Login = () => {
const onFinish = values => {
console.log('Success:', values);
};
return (
<div className="login">
<Card className="login-container">
<img className="login-logo" src={logo} alt="" />
{/* 登录表单 */}
<Form
validateTrigger='onBlur'
onFinish={onFinish}>
<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="请输入验证码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Login

封装request请求模块

下载axios依赖
npm i axios
做配置文件

import axios from 'axios'
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 }

import { request } from './request'
export { request }
使用Redux管理token

npm i react-redux @reduxjs/toolkit
配置Redux

import { createSlice } from '@reduxjs/toolkit'
import { request } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token:''
},
// 同步修改方法
reducers: {
setToken (state, action) {
state.token = action.payload
}
}
})
// 解构出actionCreater
const { setToken } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await request.post('/authorizations', loginForm)
dispatch(setToken(res.data.token))
}
}
export { fetchLogin }
export default userReducer

import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({
reducer: {
// 注册子模块
user: userReducer
}
})
入口文件引入

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.scss';
import { RouterProvider } from 'react-router-dom';
import router from './router';
import store from './store';
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
</React.StrictMode>
);
实现登录异步获取提交action

import './index.scss'
import { Card, Form, Input, Button } from 'antd'
import logo from '@/assets/logo.png'
import { useDispatch } from 'react-redux';
import { fetchLogin } from '@/store/modules/user';
const Login = () => {
const dispatch = useDispatch();
const onFinish = values => {
console.log('Success:', values);
//调用接口,提交action
dispatch(fetchLogin(values));
};
return (
<div className="login">
<Card className="login-container">
<img className="login-logo" src={logo} alt="" />
{/* 登录表单 */}
<Form
validateTrigger='onBlur'
onFinish={onFinish}>
<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="请输入验证码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Login

完善登录逻辑
import './index.scss'
import { Card, Form, Input, Button, message } from 'antd'
import logo from '@/assets/logo.png'
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 = values => {
console.log('Success:', values);
//调用接口,提交action
dispatch(fetchLogin(values));
navigate('/')
message.success('登录成功')
};
return (
<div className="login">
<Card className="login-container">
<img className="login-logo" src={logo} alt="" />
{/* 登录表单 */}
<Form
validateTrigger='onBlur'
onFinish={onFinish}>
<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="请输入验证码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Login

Token持久化

localStorage存储token
import { createSlice } from '@reduxjs/toolkit'
import { request } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token: localStorage.getItem('token_key') || ''
},
// 同步修改方法
reducers: {
setToken (state, action) {
state.token = action.payload
//localStorage存储token
localStorage.setItem('token_key', action.payload)
}
}
})
// 解构出actionCreater
const { setToken } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await request.post('/authorizations', loginForm)
dispatch(setToken(res.data.token))
}
}
export { fetchLogin }
export default userReducer

封装Token的存取删方法
写存取删方法

// 封装存取方法
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 { request } from './request'
import {setToken,getToken,clearToken} from './token'
export {
request,
setToken,
getToken,
clearToken}
修改user.js文件
vb
`import { createSlice } from '@reduxjs/toolkit'
import { request } from '@/utils'
import { setToken,getToken } from '@/utils/index'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token: getToken() || ''
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
}
}
})
// 解构出actionCreater
const { setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await request.post('/authorizations', loginForm)
dispatch(setUserInfo(res.data.token))
}
}
export { setUserInfo,fetchLogin }
export default userReducer`

Axios请求拦截器注入Token
加入token请求头

import axios from 'axios'
import { getToken } from './index'
const request = axios.create({
baseURL: 'http://geek.itheima.net/v1_0',
timeout: 5000
})
// 添加请求拦截器
request.interceptors.request.use((config)=> {
// 在发送请求之前做些什么
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, (error)=> {
return Promise.reject(error)
})
// 添加响应拦截器
request.interceptors.response.use((response)=> {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error)=> {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
export { request }
在layout调用接口测试
import { request } from "@/utils";
import { useEffect } from "react";
const Layout = () => {
useEffect(()=>{
request.get('/user/profile')
},[])
return (
<div>
<h1>Layout</h1>
</div>
);
}
export default Layout;

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

import { getToken } from "@/utils";
import { Navigate } from "react-router-dom";
const AuthRoute = ({ children }) => {
const token = getToken();
if(token){
return <>{children}</>;
}else{
return <Navigate to="/login" replace />;
}
}
export default AuthRoute;
在路由文件夹里引入

import Layout from "@/pages/Layout";
import Login from "@/pages/Login";
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;

推送git


Layout
结构创建和样式初始化

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

.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;
}
引入重置样式
npm install normalize.css
引入样式文件

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.scss';
import { RouterProvider } from 'react-router-dom';
import router from './router';
import store from './store';
import { Provider } from 'react-redux';
import 'normalize.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
</React.StrictMode>
);
修改总样式

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

二级路由配置
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
配置二级路由出口
import { Outlet } from 'react-router-dom'
<Layout className="layout-content" style={{ padding: 20 }}>
<Outlet />
</Layout>


点击菜单跳转路由
修改key为路由
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
定义函数点击跳转
const navigate = useNavigate();
const setMenuValue =(e)=>{
console.log(e);
const path = e.key
navigate(path)
}
<Menu
mode="inline"
theme="dark"
defaultSelectedKeys={['1']}
items={items}
onClick={setMenuValue}
style={{ height: '100%', borderRight: 0 }}></Menu>
全部代码
import { Layout, Menu, Popconfirm } from 'antd'
import {
HomeOutlined,
DiffOutlined,
EditOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import './index.scss'
import { Outlet, useNavigate } from 'react-router-dom'
const { Header, Sider } = Layout
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
const navigate = useNavigate();
const setMenuValue =(e)=>{
console.log(e);
const path = e.key
navigate(path)
}
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}
onClick={setMenuValue}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
{/* 这里是嵌套路由的出口 */}
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
export default GeekLayout

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

import { Outlet, useNavigate, useLocation } from 'react-router-dom'
const location = useLocation();
const selectedKey = location.pathname
<Menu
mode="inline"
theme="dark"
items={items}
onClick={setMenuValue}
selectedKeys={[selectedKey]}
style={{ height: '100%', borderRight: 0 }}></Menu>
全部代码
import { Layout, Menu, Popconfirm } from 'antd'
import {
HomeOutlined,
DiffOutlined,
EditOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import './index.scss'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
const { Header, Sider } = Layout
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
const navigate = useNavigate();
const setMenuValue =(e)=>{
console.log(e);
const path = e.key
navigate(path)
}
const location = useLocation();
const selectedKey = location.pathname
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"
items={items}
onClick={setMenuValue}
selectedKeys={[selectedKey]}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
{/* 这里是嵌套路由的出口 */}
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
export default GeekLayout

展示个人信息
添加用户信息和用户接口

import { createSlice } from '@reduxjs/toolkit'
import { request } from '@/utils'
import { setToken,getToken } from '@/utils/index'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token: getToken() || '',
adminInfo: {}
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
},
setAdminInfo (state, action) {
state.adminInfo = action.payload
}
}
})
// 解构出actionCreater
const { setUserInfo, setAdminInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await request.post('/authorizations', loginForm)
dispatch(setUserInfo(res.data.token))
}
}
const fetchAdminInfo = () => {
return async (dispatch) => {
const res = await request.get('/user/profile')
dispatch(setAdminInfo(res.data))
}
}
export { setUserInfo, setAdminInfo, fetchLogin, fetchAdminInfo }
export default userReducer
调用接口和state的name数据
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchAdminInfo } from '@/store/modules/user'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
const name = useSelector(state=>state.user.adminInfo.name)
const dispatch = useDispatch();
useEffect(() => {
//获取用户信息
dispatch(fetchAdminInfo())
}, [dispatch])
<span className="user-name">{name}</span>
全部代码
import { Layout, Menu, Popconfirm } from 'antd'
import {
HomeOutlined,
DiffOutlined,
EditOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import './index.scss'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchAdminInfo } from '@/store/modules/user'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
const { Header, Sider } = Layout
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
const navigate = useNavigate();
const setMenuValue =(e)=>{
console.log(e);
const path = e.key
navigate(path)
}
const dispatch = useDispatch();
const location = useLocation();
const selectedKey = location.pathname
const name = useSelector(state=>state.user.adminInfo.name)
useEffect(() => {
//获取用户信息
dispatch(fetchAdminInfo())
}, [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"
items={items}
onClick={setMenuValue}
selectedKeys={[selectedKey]}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
{/* 这里是嵌套路由的出口 */}
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
export default GeekLayout

退出登录实现
绑定弹窗,设置函数相应退出
<Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={onConfirm}>
<LogoutOutlined /> 退出
</Popconfirm>
写Redux里的数据清空方式

import { createSlice } from '@reduxjs/toolkit'
import { request } from '@/utils'
import { setToken,getToken ,clearToken} from '@/utils/index'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token: getToken() || '',
adminInfo: {}
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
},
setAdminInfo (state, action) {
state.adminInfo = action.payload
},
clearAdminInfo (state) {
state.adminInfo = {}
state.token = ''
// 清除本地
clearToken()
}
}
})
// 解构出actionCreater
const { setUserInfo, setAdminInfo,clearAdminInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await request.post('/authorizations', loginForm)
dispatch(setUserInfo(res.data.token))
}
}
const fetchAdminInfo = () => {
return async (dispatch) => {
const res = await request.get('/user/profile')
dispatch(setAdminInfo(res.data))
}
}
export { setUserInfo, setAdminInfo,clearAdminInfo, fetchLogin, fetchAdminInfo }
export default userReducer
调用清空切跳转
const onConfirm=()=>{
//退出登录
dispatch(clearAdminInfo())
navigate('/login')
}
全部代码
import { Layout, Menu, Popconfirm } from 'antd'
import {
HomeOutlined,
DiffOutlined,
EditOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import './index.scss'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchAdminInfo ,clearAdminInfo} from '@/store/modules/user'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
const { Header, Sider } = Layout
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
const navigate = useNavigate();
const setMenuValue =(e)=>{
console.log(e);
const path = e.key
navigate(path)
}
const dispatch = useDispatch();
const location = useLocation();
const selectedKey = location.pathname
const name = useSelector(state=>state.user.adminInfo.name)
useEffect(() => {
//获取用户信息
dispatch(fetchAdminInfo())
}, [dispatch])
const onConfirm=()=>{
//退出登录
dispatch(clearAdminInfo())
navigate('/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={onConfirm}>
<LogoutOutlined /> 退出
</Popconfirm>
</span>
</div>
</Header>
<Layout>
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
items={items}
onClick={setMenuValue}
selectedKeys={[selectedKey]}
style={{ height: '100%', borderRight: 0 }}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
{/* 这里是嵌套路由的出口 */}
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
export default GeekLayout

处理token失效

import axios from 'axios'
import { getToken } from './index'
import { clearToken } from './index'
import router from '@/router'
const request = axios.create({
baseURL: 'http://geek.itheima.net/v1_0',
timeout: 5000
})
// 添加请求拦截器
request.interceptors.request.use((config)=> {
// 在发送请求之前做些什么
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, (error)=> {
return Promise.reject(error)
})
// 添加响应拦截器
request.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)
})
export { request }

Home部分
Echarts基础图表渲染
下载依赖
npm i echarts
官网
https://echarts.apache.org/zh/index.html

import * as echarts from 'echarts';
import { useEffect,useRef } from 'react';
const Home = () => {
const chartRef = useRef(null);
useEffect(()=>{
// 1. 生成实例
const chartDom = chartRef.current;
const myChart = echarts.init(chartDom);
// 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. 渲染参数
option && myChart.setOption(option);
})
return (
<div>
<div ref={chartRef} style={{width:'400px',height:'300px'}}></div>
</div>
)
}
export default Home

Echarts组件封装实现
封装一个图组件

import * as echarts from 'echarts';
import { useEffect,useRef } from 'react';
const BarChart = ({chartName}) => {
const chartRef = useRef(null);
useEffect(()=>{
// 1. 生成实例
const chartDom = chartRef.current;
const myChart = echarts.init(chartDom);
// 2. 准备图表参数
const option = {
title:{ text:chartName},
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. 渲染参数
option && myChart.setOption(option);
})
return (
<div ref={chartRef} style={{width:'400px',height:'300px'}}></div>
);
}
export default BarChart;
引用并且传参

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

拓展-API模块封装
在api里习接口封装

import { request } from "@/utils";
export function LoginApi(data){
return request({
url:'/authorizations',
method:'post',
data:data
})
}
export function GetUserInfoApi(){
return request({
url:'/user/profile',
method:'get'
})
}
修改Redux调用的接口

import { createSlice } from '@reduxjs/toolkit'
// import { request } from '@/utils'
import { setToken,getToken ,clearToken} from '@/utils/index'
import { LoginApi,GetUserInfoApi } from '@/apis/user'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token: getToken() || '',
adminInfo: {}
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
},
setAdminInfo (state, action) {
state.adminInfo = action.payload
},
clearAdminInfo (state) {
state.adminInfo = {}
state.token = ''
// 清除本地
clearToken()
}
}
})
// 解构出actionCreater
const { setUserInfo, setAdminInfo,clearAdminInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await LoginApi(loginForm)
// 存入本地
dispatch(setUserInfo(res.data.token))
}
}
const fetchAdminInfo = () => {
return async (dispatch) => {
const res = await GetUserInfoApi()
dispatch(setAdminInfo(res.data))
}
}
export { setUserInfo, setAdminInfo,clearAdminInfo, fetchLogin, fetchAdminInfo }
export default userReducer
基础文章发布
创建并熟悉基础结构

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
.publish {
position: relative;
}
.ant-upload-list {
.ant-upload-list-picture-card-container,
.ant-upload-select {
width: 146px;
height: 146px;
}
}

准备富文本编辑器
强制下载依赖
npm i react-quill@2.0.0-beta.2 --legacy-peer-deps
因为现在默认下载的react都是19多的所以删除node-module文件依赖,重新下载
npm install react@^18.3.1 react-dom@^18.3.1
就可以正常使用富文本编辑器

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'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
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: '请输入文章内容' }]}
>
<ReactQuill
className="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
.publish {
position: relative;
}
.ant-upload-list {
.ant-upload-list-picture-card-container,
.ant-upload-select {
width: 146px;
height: 146px;
}
}
.publish-quill {
.ql-editor {
min-height: 300px;
}
}

频道列表获取渲染
封装列表接口

import { request } from "@/utils";
export function publishListApi(){
return request({
url:"/channels",
method:"GET"
})
}
调用接口并且遍历

import { useState,useEffect } from 'react'
import { publishListApi } from '@/apis/publish'
const [channelList, setChannelList] = useState([])
useEffect(() => {
const getchannelList = async () => {
const res = await publishListApi()
console.log(res.data.channels);
setChannelList(res.data.channels)
}
getchannelList()
}, [])
全部代码
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'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { useState,useEffect } from 'react'
import { publishListApi } from '@/apis/publish'
const { Option } = Select
const Publish = () => {
const [channelList, setChannelList] = useState([])
useEffect(() => {
const getchannelList = async () => {
const res = await publishListApi()
console.log(res.data.channels);
setChannelList(res.data.channels)
}
getchannelList()
}, [])
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 }}>
{channelList.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
)
})}
</Select>
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="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

收集表单数据提交表单
封装提交表单接口
import { request } from "@/utils";
//获取频道列表
export function publishListApi(){
return request({
url:"/channels",
method:"GET"
})
}
//提交表单接口
export function publishSubmitApi(data){
return request({
url:"/mp/articles?draft=false",
method:"POST",
data
})
}
根据接口格式获取数据,并且点击提交数据
const onFinish =(formvalues)=>{
console.log(formvalues);
//结构格式
const {title,channel_id,content} = formvalues
//定义格式
const params = {
title,
content,
cover:{
type:1,
images:[]
},
channel_id,
}
//调用接口
publishSubmitApi(params)
}
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 1 }}
onFinish={onFinish}
>

文章封面
上传文章封面基础功能实现
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.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>
<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>
全部代码
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'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { useState,useEffect } from 'react'
import { publishListApi ,publishSubmitApi} from '@/apis/publish'
const { Option } = Select
const Publish = () => {
const [channelList, setChannelList] = useState([])
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
}
useEffect(() => {
const getchannelList = async () => {
const res = await publishListApi()
console.log(res.data.channels);
setChannelList(res.data.channels)
}
getchannelList()
}, [])
const onFinish =(formvalues)=>{
console.log(formvalues);
//结构格式
const {title,channel_id,content} = formvalues
//定义格式
const params = {
title,
content,
cover:{
type:0,
images:[]
},
channel_id,
}
//调用接口
publishSubmitApi(params)
}
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '发布文章' },
]}
/>
}
>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 1 }}
onFinish={onFinish}
>
<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 }}>
{channelList.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
)
})}
</Select>
</Form.Item>
<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>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="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
实现切换封面类型和控制上传数量
定义类型状态并且保存
//保存类型的状态
const [type, setType] = useState(0)
//定义三种上传图片类型
const onTypeChange = (e) => {
setType(e.target.value)
}
状态>0显示
{type > 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>}
设置默认值为0
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 0 }}
onFinish={onFinish}
>
控制上传数量
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={type}
>
全部代码
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'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { useState,useEffect } from 'react'
import { publishListApi ,publishSubmitApi} from '@/apis/publish'
const { Option } = Select
const Publish = () => {
const [channelList, setChannelList] = useState([])
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
}
//保存类型的状态
const [type, setType] = useState(0)
//定义三种上传图片类型
const onTypeChange = (e) => {
setType(e.target.value)
}
useEffect(() => {
const getchannelList = async () => {
const res = await publishListApi()
console.log(res.data.channels);
setChannelList(res.data.channels)
}
getchannelList()
}, [])
const onFinish =(formvalues)=>{
console.log(formvalues);
//结构格式
const {title,channel_id,content} = formvalues
//定义格式
const params = {
title,
content,
cover:{
type:0,
images:[]
},
channel_id,
}
//调用接口
publishSubmitApi(params)
}
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '发布文章' },
]}
/>
}
>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 0 }}
onFinish={onFinish}
>
<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 }}>
{channelList.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
)
})}
</Select>
</Form.Item>
<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>
{type > 0 &&
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={type}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="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

发布带封面的文章
填补格式
const params = {
title,
content,
cover:{
type:type,
images:imageList.map(item=>item.response.data.url)
},
channel_id,
}
判断图片类型是否和提交的类型一致
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Input,
Upload,
Space,
Select,
message,
} from 'antd'
//表单提交
const onFinish =(formvalues)=>{
//判断提交图片和类型是否一致
if(imageList.length!==type) return message.warning('请上传对应类型的图片')
//结构格式
const {title,channel_id,content} = formvalues
//定义格式
const params = {
title,
content,
cover:{
type:type,
images:imageList.map(item=>item.response.data.url)
},
channel_id,
}
//调用接口
publishSubmitApi(params)
}
全部代码
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Input,
Upload,
Space,
Select,
message,
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'
import './index.scss'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { useState,useEffect } from 'react'
import { publishListApi ,publishSubmitApi} from '@/apis/publish'
const { Option } = Select
const Publish = () => {
const [channelList, setChannelList] = useState([])
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
}
//保存类型的状态
const [type, setType] = useState(0)
//定义三种上传图片类型
const onTypeChange = (e) => {
setType(e.target.value)
}
useEffect(() => {
const getchannelList = async () => {
const res = await publishListApi()
console.log(res.data.channels);
setChannelList(res.data.channels)
}
getchannelList()
}, [])
//表单提交
const onFinish =(formvalues)=>{
//判断提交图片和类型是否一致
if(imageList.length!==type) return message.warning('请上传对应类型的图片')
//结构格式
const {title,channel_id,content} = formvalues
//定义格式
const params = {
title,
content,
cover:{
type:type,
images:imageList.map(item=>item.response.data.url)
},
channel_id,
}
//调用接口
publishSubmitApi(params)
}
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '发布文章' },
]}
/>
}
>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 0 }}
onFinish={onFinish}
>
<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 }}>
{channelList.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
)
})}
</Select>
</Form.Item>
<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>
{type > 0 &&
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={type}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="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


文章列表
静态结构创建

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'
//列表组件和icon
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import img404 from '@/assets/error.png'
const { Option } = Select
const { RangePicker } = DatePicker
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={
<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>
{/* */}
<Card title={`根据筛选条件共查询到 count 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={data} />
</Card>
</div>
</div>
)
}
export default Article

使用hook渲染频道数据
在hooks文件夹内封装频道数据

import { publishListApi } from "@/apis/publish";
import { useState, useEffect } from "react";
function useChannel() {
//抽象出逻辑
const [channelList, setChannelList] = useState([])
useEffect(() => {
const getchannelList = async () => {
const res = await publishListApi()
console.log(res.data.channels);
setChannelList(res.data.channels)
}
getchannelList()
}, [])
//return出去
return {
channelList
}
}
export { useChannel }
在文件中引入
import {useChannel} from '@/hooks/useChannel'
调用频道数据并且遍历列表
//获取频道列表
const { channelList } = useChannel()
<Form.Item label="频道" name="channel_id">
<Select
placeholder="请选择文章频道"
defaultValue="lucy"
style={{ width: 120 }}
>
{channelList.map(item =>
<Option value={item.id} key={item.id}>
{item.name}
</Option>
)}
</Select>
</Form.Item>

渲染table表格
封装table列表接口
import { request } from "@/utils";
export function ArticleListApi(params) {
return request({
url: "/mp/articles",
method: "GET",
params,
});
}
调用接口并且存储数量和列表

//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi()
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[])
{/* */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} />
</Card>
全部代码
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'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react'
import {useChannel} from '@/hooks/useChannel'
import { ArticleListApi } from '@/apis/artcle'
import img404 from '@/assets/error.png'
const { Option } = Select
const { RangePicker } = DatePicker
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资源解决方案'
}
]
//获取频道列表
const { channelList } = useChannel()
//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi()
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[])
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 }}
>
{channelList.map(item =>
<Option value={item.id} key={item.id}>
{item.name}
</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>
{/* */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} />
</Card>
</div>
</div>
)
}
export default Article

适配文章状态
定义状态
const status = {
1:<Tag color="warning">审核中</Tag>,
2:<Tag color="success">审核通过</Tag>,
}
// 准备列数据
const columns = [
{
title: '状态',
dataIndex: 'status',
render: data => status[data]
},
全部代码
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'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react'
import {useChannel} from '@/hooks/useChannel'
import { ArticleListApi } from '@/apis/artcle'
import img404 from '@/assets/error.png'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
//定义状态
const status = {
1:<Tag color="warning">审核中</Tag>,
2:<Tag color="success">审核通过</Tag>,
}
// 准备列数据
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 => status[data]
},
{
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资源解决方案'
}
]
//获取频道列表
const { channelList } = useChannel()
//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi()
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[])
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 }}
>
{channelList.map(item =>
<Option value={item.id} key={item.id}>
{item.name}
</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>
{/* */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} />
</Card>
</div>
</div>
)
}
export default Article
筛选功能实现

准备完整的请求参数对象
const [dataParams,setDataParams] = useState({
status:'',
channel_id:'',
begin_pubdate:'',
end_pubdate:'',
page:1,
per_page:4
})
获取用户选择的表单数据
//获取用户选择的数据
const onFinish = (values) => {
console.log('Success:', values);
}
<Form initialValues={{ status: '' }} onFinish={onFinish}>
<Form.Item label="状态" name="status">
<Radio.Group>
<Radio value={''}>全部</Radio>
<Radio value={0}>草稿</Radio>
<Radio value={2}>审核通过</Radio>
</Radio.Group>
</Form.Item>
把表单数据放置到接口对应的字段中
//获取用户选择的数据
const onFinish = (values) => {
console.log('Success:', values);
setDataParams({
...dataParams,
status:values.status,
channel_id:values.channel_id,
begin_pubdate:values.date[0].format('YYYY-MM-DD'),
end_pubdate:values.date[1].format('YYYY-MM-DD'),
})
}
重新调用文章列表接口渲染Table列表
//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi(dataParams)
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[dataParams])
全部代码
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'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react'
import {useChannel} from '@/hooks/useChannel'
import { ArticleListApi } from '@/apis/artcle'
import img404 from '@/assets/error.png'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
//定义状态
const status = {
1:<Tag color="warning">审核中</Tag>,
2:<Tag color="success">审核通过</Tag>,
}
// 准备列数据
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 => status[data]
},
{
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资源解决方案'
// }
// ]
//获取频道列表
const { channelList } = useChannel()
//准备完整请求参数对象
const [dataParams,setDataParams] = useState({
status:'',
channel_id:'',
begin_pubdate:'',
end_pubdate:'',
page:1,
per_page:4
})
//获取用户选择的数据
const onFinish = (values) => {
console.log('Success:', values);
setDataParams({
...dataParams,
status:values.status,
channel_id:values.channel_id,
begin_pubdate:values.date[0].format('YYYY-MM-DD'),
end_pubdate:values.date[1].format('YYYY-MM-DD'),
})
}
//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi(dataParams)
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[dataParams])
return (
<div>
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '文章列表' },
]} />
}
style={{ marginBottom: 20 }}
>
<Form initialValues={{ status: '' }} onFinish={onFinish}>
<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 }}
>
{channelList.map(item =>
<Option value={item.id} key={item.id}>
{item.name}
</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>
{/* */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} />
</Card>
</div>
</div>
)
}
export default Article

分页功能实现

//分页
const onChangePage = (page)=>{
console.log(page);
setDataParams({
...dataParams,
page
})
}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} pagination={{
total: count,
pageSize: dataParams.per_page,
current: dataParams.page,
showTotal: total => `共 ${total} 条`,
onChange:onChangePage
}}/>
</Card>
全部代码
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'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react'
import {useChannel} from '@/hooks/useChannel'
import { ArticleListApi } from '@/apis/artcle'
import img404 from '@/assets/error.png'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
//定义状态
const status = {
1:<Tag color="warning">审核中</Tag>,
2:<Tag color="success">审核通过</Tag>,
}
// 准备列数据
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 => status[data]
},
{
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>
)
}
}
]
//获取频道列表
const { channelList } = useChannel()
//准备完整请求参数对象
const [dataParams,setDataParams] = useState({
status:'',
channel_id:'',
begin_pubdate:'',
end_pubdate:'',
page:1,
per_page:4
})
//获取用户选择的数据
const onFinish = (values) => {
console.log('Success:', values);
setDataParams({
...dataParams,
status:values.status,
channel_id:values.channel_id,
begin_pubdate:values.date[0].format('YYYY-MM-DD'),
end_pubdate:values.date[1].format('YYYY-MM-DD'),
})
}
//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi(dataParams)
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[dataParams])
//分页
const onChangePage = (page)=>{
console.log(page);
setDataParams({
...dataParams,
page
})
}
return (
<div>
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '文章列表' },
]} />
}
style={{ marginBottom: 20 }}
>
<Form initialValues={{ status: '' }} onFinish={onFinish}>
<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 }}
>
{channelList.map(item =>
<Option value={item.id} key={item.id}>
{item.name}
</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>
{/* */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} pagination={{
total: count,
pageSize: dataParams.per_page,
current: dataParams.page,
showTotal: total => `共 ${total} 条`,
onChange:onChangePage
}}/>
</Card>
</div>
</div>
)
}
export default 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 => status[data]
},
{
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 />} />
<Popconfirm
title="确认删除该条文章吗?"
onConfirm={()=>onConfirm(data)}
okText="确认"
cancelText="取消"
>
<Button
type="primary"
danger
shape="circle"
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space>
)
}
}
]
封装接口
export function ArticleDeleteApi(id) {
return request({
url: `/mp/articles/${id}`,
method: "DELETE",
})
}
得到文章id,使用id调用删除接口
//删除文章
const onConfirm =async(value)=>{
console.log(value.id);
await ArticleDeleteApi(value.id)
setDataParams({
...dataParams,
})
}
全部代码
import { Link } from 'react-router-dom'
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select,Popconfirm } from 'antd'
import locale from 'antd/es/date-picker/locale/zh_CN'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react'
import {useChannel} from '@/hooks/useChannel'
import { ArticleListApi, ArticleDeleteApi } from '@/apis/artcle'
import img404 from '@/assets/error.png'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
//定义状态
const status = {
1:<Tag color="warning">审核中</Tag>,
2:<Tag color="success">审核通过</Tag>,
}
// 准备列数据
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 => status[data]
},
{
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 />} />
<Popconfirm
title="确认删除该条文章吗?"
onConfirm={()=>onConfirm(data)}
okText="确认"
cancelText="取消"
>
<Button
type="primary"
danger
shape="circle"
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space>
)
}
}
]
//获取频道列表
const { channelList } = useChannel()
//准备完整请求参数对象
const [dataParams,setDataParams] = useState({
status:'',
channel_id:'',
begin_pubdate:'',
end_pubdate:'',
page:1,
per_page:4
})
//获取用户选择的数据
const onFinish = (values) => {
console.log('Success:', values);
setDataParams({
...dataParams,
status:values.status,
channel_id:values.channel_id,
begin_pubdate:values.date[0].format('YYYY-MM-DD'),
end_pubdate:values.date[1].format('YYYY-MM-DD'),
})
}
//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi(dataParams)
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[dataParams])
//分页
const onChangePage = (page)=>{
console.log(page);
setDataParams({
...dataParams,
page
})
}
//删除文章
const onConfirm =async(value)=>{
console.log(value.id);
await ArticleDeleteApi(value.id)
setDataParams({
...dataParams,
})
}
return (
<div>
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '文章列表' },
]} />
}
style={{ marginBottom: 20 }}
>
<Form initialValues={{ status: '' }} onFinish={onFinish}>
<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 }}
>
{channelList.map(item =>
<Option value={item.id} key={item.id}>
{item.name}
</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>
{/* */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} pagination={{
total: count,
pageSize: dataParams.per_page,
current: dataParams.page,
showTotal: total => `共 ${total} 条`,
onChange:onChangePage
}}/>
</Card>
</div>
</div>
)
}
export default Article
编辑文章跳转
const navagite = useNavigate()
<Space size="middle">
<Button type="primary" shape="circle" icon={<EditOutlined />} onClick={() => navagite(`/publish?id=${data.id}`)} />
<Popconfirm
title="确认删除该条文章吗?"
onConfirm={()=>onConfirm(data)}
okText="确认"
cancelText="取消"
>
全部代码
import { Link, useNavigate } from 'react-router-dom'
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select,Popconfirm } from 'antd'
import locale from 'antd/es/date-picker/locale/zh_CN'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react'
import {useChannel} from '@/hooks/useChannel'
import { ArticleListApi, ArticleDeleteApi } from '@/apis/artcle'
import img404 from '@/assets/error.png'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
const navagite = useNavigate()
//定义状态
const status = {
1:<Tag color="warning">审核中</Tag>,
2:<Tag color="success">审核通过</Tag>,
}
// 准备列数据
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 => status[data]
},
{
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 />} onClick={() => navagite(`/publish?id=${data.id}`)} />
<Popconfirm
title="确认删除该条文章吗?"
onConfirm={()=>onConfirm(data)}
okText="确认"
cancelText="取消"
>
<Button
type="primary"
danger
shape="circle"
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space>
)
}
}
]
//获取频道列表
const { channelList } = useChannel()
//准备完整请求参数对象
const [dataParams,setDataParams] = useState({
status:'',
channel_id:'',
begin_pubdate:'',
end_pubdate:'',
page:1,
per_page:4
})
//获取用户选择的数据
const onFinish = (values) => {
console.log('Success:', values);
setDataParams({
...dataParams,
status:values.status,
channel_id:values.channel_id,
begin_pubdate:values.date[0].format('YYYY-MM-DD'),
end_pubdate:values.date[1].format('YYYY-MM-DD'),
})
}
//获取文章列表,并且存储数量
const [count,setCount] = useState(0)
const [Articlelist , setArticlelist] = useState([])
useEffect(() => {
const getacticlelist = async () => {
const res = await ArticleListApi(dataParams)
setArticlelist(res.data.results)
setCount(res.data.total_count)
}
getacticlelist()
},[dataParams])
//分页
const onChangePage = (page)=>{
console.log(page);
setDataParams({
...dataParams,
page
})
}
//删除文章
const onConfirm =async(value)=>{
console.log(value.id);
await ArticleDeleteApi(value.id)
setDataParams({
...dataParams,
})
}
return (
<div>
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '文章列表' },
]} />
}
style={{ marginBottom: 20 }}
>
<Form initialValues={{ status: '' }} onFinish={onFinish}>
<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 }}
>
{channelList.map(item =>
<Option value={item.id} key={item.id}>
{item.name}
</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>
{/* */}
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
<Table rowKey="id" columns={columns} dataSource={Articlelist} pagination={{
total: count,
pageSize: dataParams.per_page,
current: dataParams.page,
showTotal: total => `共 ${total} 条`,
onChange:onChangePage
}}/>
</Card>
</div>
</div>
)
}
export default Article

编辑文章
回填基础数据

封装接口

export function publishUpdateApi(id){
return request({
url:`/mp/articles/${id}`,
// method:"GET",
})
}

//回填数据
const [searchParams] = useSearchParams()
const artcleId = searchParams.get('id')
//获取实例
const [form] = Form.useForm()
useEffect(()=>{
async function getArticle (){
const res = await publishUpdateApi(artcleId)
// console.log(res)
form.setFieldsValue(res.data)
}
getArticle()
},[artcleId,form])
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 0 }}
onFinish={onFinish}
form={form}
>

回填封面信息

//回填数据
const [searchParams] = useSearchParams()
const artcleId = searchParams.get('id')
//获取实例
const [form] = Form.useForm()
useEffect(()=>{
async function getArticle (){
const res = await publishUpdateApi(artcleId)
const data = res.data
const {cover} = data
form.setFieldsValue({
...data,
type:cover.type
})
//回填图片列表
setType(cover.type)
//回填图片路径
setImageList(cover.images.map(url=>{
return {url}
}))
}
getArticle()
},[artcleId,form])
{type > 0 &&
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={type}
fileList={imageList}
>
全部代码
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Input,
Upload,
Space,
Select,
message,
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link, useSearchParams } from 'react-router-dom'
import './index.scss'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { useEffect, useState } from 'react'
import { publishSubmitApi,publishUpdateApi} from '@/apis/publish'
import { useChannel } from '@/hooks/useChannel'
const { Option } = Select
const Publish = () => {
// const [channelList, setChannelList] = useState([])
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
}
//保存类型的状态
const [type, setType] = useState(0)
//定义三种上传图片类型
const onTypeChange = (e) => {
setType(e.target.value)
}
//回填数据
const [searchParams] = useSearchParams()
const artcleId = searchParams.get('id')
//获取实例
const [form] = Form.useForm()
useEffect(()=>{
async function getArticle (){
const res = await publishUpdateApi(artcleId)
const data = res.data
const {cover} = data
form.setFieldsValue({
...data,
type:cover.type
})
//回填图片列表
setType(cover.type)
//回填图片路径
setImageList(cover.images.map(url=>{
return {url}
}))
}
getArticle()
},[artcleId,form])
//获取频道列表
const {channelList} = useChannel()
//表单提交
const onFinish =(formvalues)=>{
//判断提交图片和类型是否一致
if(imageList.length!==type) return message.warning('请上传对应类型的图片')
//结构格式
const {title,channel_id,content} = formvalues
//定义格式
const params = {
title,
content,
cover:{
type:type,
images:imageList.map(item=>item.response.data.url)
},
channel_id,
}
//调用接口
publishSubmitApi(params)
}
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: '发布文章' },
]}
/>
}
>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 0 }}
onFinish={onFinish}
form={form}
>
<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 }}>
{channelList.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
)
})}
</Select>
</Form.Item>
<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>
{type > 0 &&
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={type}
fileList={imageList}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="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

根据id适配状态
//如果有id就回填数据
if(artcleId) {
getArticle()
}
},[artcleId,form])
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: artcleId ? '修改文章' : '发布文章' },
]}
/>
}
>


更新文章
修改新增时的图片的格式,根据有无response判断加入图片参数
//定义格式
const params = {
title,
content,
cover:{
type:type,
images:imageList.map(item=>{
if (item.response) {
return item.response.data.url
} else {
return item.url
}
})
},
channel_id,
}
封装编辑接口
export function publishUpdateSubmitApi(data){
return request({
url:`/mp/articles/${data.id}?draft=false`,
method:"PUT",
data
})
}
判断点击按钮后,接口是新增还是编辑
//调用接口
//判断是否有id,有就调用修改接口,没有就调用发布接口
if(artcleId) {
//更新接口
publishUpdateSubmitApi({
...params,
id:artcleId
})
}else{
//发布接口
publishSubmitApi(params)
}
}
全部代码
import {
Card,
Breadcrumb,
Form,
Button,
Radio,
Input,
Upload,
Space,
Select,
message,
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link, useSearchParams } from 'react-router-dom'
import './index.scss'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
import { useEffect, useState } from 'react'
import { publishSubmitApi,publishUpdateApi,publishUpdateSubmitApi} from '@/apis/publish'
import { useChannel } from '@/hooks/useChannel'
const { Option } = Select
const Publish = () => {
// const [channelList, setChannelList] = useState([])
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
}
//保存类型的状态
const [type, setType] = useState(0)
//定义三种上传图片类型
const onTypeChange = (e) => {
setType(e.target.value)
}
//回填数据
const [searchParams] = useSearchParams()
const artcleId = searchParams.get('id')
//获取实例
const [form] = Form.useForm()
useEffect(()=>{
async function getArticle (){
const res = await publishUpdateApi(artcleId)
const data = res.data
const {cover} = data
form.setFieldsValue({
...data,
type:cover.type
})
//回填图片列表
setType(cover.type)
//回填图片路径
setImageList(cover.images.map(url=>{
return {url}
}))
}
//如果有id就回填数据
if(artcleId) {
getArticle()
}
},[artcleId,form])
//获取频道列表
const {channelList} = useChannel()
//表单提交
const onFinish =(formvalues)=>{
//判断提交图片和类型是否一致
if(imageList.length!==type) return message.warning('请上传对应类型的图片')
//结构格式
const {title,channel_id,content} = formvalues
//定义格式
const params = {
title,
content,
cover:{
type:type,
images:imageList.map(item=>{
if (item.response) {
return item.response.data.url
} else {
return item.url
}
})
},
channel_id,
}
//调用接口
//判断是否有id,有就调用修改接口,没有就调用发布接口
if(artcleId) {
//更新接口
publishUpdateSubmitApi({
...params,
id:artcleId
})
}else{
//发布接口
publishSubmitApi(params)
}
}
return (
<div className="publish">
<Card
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>首页</Link> },
{ title: artcleId ? '修改文章' : '发布文章' },
]}
/>
}
>
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
initialValues={{ type: 0 }}
onFinish={onFinish}
form={form}
>
<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 }}>
{channelList.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
)
})}
</Select>
</Form.Item>
<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>
{type > 0 &&
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={type}
fileList={imageList}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</Form.Item>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="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


完成

项目打包和本地预览

npm run build
npm i -g serve
serve -s ./build

