React实操案例(四)

极客园

初始化项目

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
相关推荐
zhengfei6112 小时前
一种综合性的现代架构模型,用于集成平台解决方案和工具,以支持专业的红队。
开发语言·人工智能·网络安全·架构·信息与通信
阿珊和她的猫2 小时前
简述 React 的虚拟 DOM 机制
前端·react.js·前端框架
zuoyou-HPU2 小时前
QT C++开发知识点剖析
开发语言·c++·qt
Easonmax2 小时前
基础入门 React Native 鸿蒙跨平台开发:简单模拟一个加油站
react native·react.js·harmonyos
草莓熊Lotso2 小时前
Qt 按钮与显示类控件实战:从交互到展示全攻略
大数据·开发语言·c++·人工智能·qt·microsoft·交互
莫问前路漫漫2 小时前
Java static 与 final 详解(简单易懂)
java·开发语言
爱学习的阿磊2 小时前
模板编译期排序算法
开发语言·c++·算法
冰暮流星2 小时前
javascript之for-of循环
开发语言·javascript·ecmascript