react+vite+antD+reduce+echarts项目完整记录

react+vite+antD+reduce+echarts项目完整记录

之前写前端项目,都是用的vue,从最开始的vue2到后来的vue3,断断续续写了3年,打包工具也从webpack转到了vite,全局数据管理工具从vuex转到了pinia。总体而言,vue3对比vue2,有非常明显的提升,vite比webpack打包的速度更是快了无数倍,至于pinia和vuex,因人而异,我更喜欢pinia,组合式api的写法深得我心。总而言之一句话,我是全方面拥抱了vue3的新技术栈,当然,除了TS,TS对后端比较友好,我只能算半个后端,用不用无所谓。时代在前进,技术在发展,如果永远守着一套陈旧的技术,找各种理由为自己辩解,实在是不明智的选择。

一直想学一下react,中途学过几次,因为平时工作事情太多不得不停下来。刚开始接触jsx,我是抵制的,vue把html、js和css进行了严格的区分,并摆脱了原生的dom操作,jsx却又把这些混在一起,写代码的时候让我感觉像吃了si一样难受。学完之后依然觉得很难受,但为啥我还是坚持要学react呢,这么几个原因:

  • 从全球来看,react是最火的前端框架,vue只在国内火,我在看国外的一些项目的源码时,发现自己完全看不懂在写什么,甚至国内,有些开源项目也只出了react的包,比如mapv
  • 换一种框架,扩展一下自己的技能树
  • 熟悉原生js

花了3天速刷了一遍B站黑马前端讲师的课,并跟着完整写了一个非常简单的项目,后端接口也是用的黑马的,感谢黑马,记录一下完整的过程,为自己后面写项目提供参考,也为后来人提供参考

项目最终界面:

  1. 登录界面

image-20240328115752843

  1. 首页

image-20240328115845338

  1. 文章管理

image-20240328115913121

  1. 创建文章

image-20240328115938645

目前就这些了,以下进入正题

〇、代码仓库地址

https://gitee.com/hgandzl/react-vite

一、创建项目并配置基础环境

1. vite创建项目

黑马老师是基于CRA创建项目,应该是和webpack相关的技术,没深入了解,我是用的vite

vite创建前端项目的指令

npm create vite@latest

创建过程如下:

image-20240328120528329

vscode打开创建的项目,执行npm iimage-20240328120722097执行npm run dev,即可打开默认的vite+react项目

2. 整理项目目录

项目src文件夹下依次创建如下文件夹

-src
  -apis           项目接口函数
  -assets         项目资源文件,比如,图片等
  -components     通用组件
  -pages          页面组件
  -router		  路由
  -store          集中状态管理
  -utils          工具,比如,token、axios 的封装等
  -App.jsx        根组件
  -index.scss     全局样式
  -main.jsx       项目入口

删除无关的文件,只保留App.jsx和main.jsx,并删除相关引入

删除main.jsx中的严格节点模式

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";

ReactDOM.createRoot(document.getElementById("root")).render(<App />);

删除App.jsx中无关的代码,保留基础组件

function App() {
  return <>app</>;
}

export default App;

3. 使用scss预处理器

实现步骤

  1. 安装解析 sass 的包:npm i sass -D
  2. 创建全局样式文件:index.scss

index.scss

* {
    margin: 0;
    padding: 0;
}

项目入口文件引入index.scss

4. 使用Ant Design作为UI框架

实现步骤

  1. 安装 antd 组件库:npm i antd
  2. 页面上导入并使用

5. 配置基础路由

实现步骤

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

  2. 准备 LayoutLogin俩个基础组件

    pages目录下新建两个组件,分别是pages/Layout/index.jsx和pages/Login/index.jsx,并同步新建样式文件

    pages/Layout/index.jsx

    const Layout = () => {
      return <div>this is layout</div>
    }
    export default Layout
    

    pages/Login/index.jsx

    const Login = () => {
      return <div>this is login</div>
    }
    export default Login
    
  3. 配置路由

    router文件夹下新建index.jsx文件,并配置如下基础路由

    import { createBrowserRouter } from 'react-router-dom'
    
    import Login from '../pages/Login'
    import Layout from '../pages/Layout'
    
    const router = createBrowserRouter([
      {
        path: '/',
        element: <Layout />,
      },
      {
        path: '/login',
        element: <Login />,
      },
    ])
    
    export default router
    
  4. 全局挂载路由

    和vue项目类似,路由要全局挂载

    main.jsx

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

二、编写登录页面

1. 使用antd搭建基本结构

实现步骤

  1. Login/index.js 中创建登录页面基本结构

    import "./index.scss";
    import { Card, Form, Input, Button } from "antd";
    import logo from "../../assets/global.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;
    
    
  2. 在 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: 400px;
            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;
        }
    }
    

启动项目,地址输入登录页面路由,显示如下

image-20240328140322996

2. 实现表单校验功能

实现步骤

  1. 为 Form 组件添加 validateTrigger 属性,指定校验触发时机的集合
  2. 为 Form.Item 组件添加 name 属性,这是为了能取到表单项里面的值
  3. 为 Form.Item 组件添加 rules 属性,用来添加表单校验规则对象,这与elementplus的验证机制高度相似

整体实现代码

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>
  )
}

3. 获取登录form的表单数据

实现步骤

  1. 为 Form 组件添加 onFinish 属性,该事件会在点击登录按钮时触发。其实这个onFinish也是button中的submit绑定的,也就是说点击submit按钮时,就会触发onFinish方法
  2. 创建 onFinish 函数,通过函数参数 values 拿到表单值,onFinish函数传递默认参数,参数就是表单内的每一项数据
const onFinish = (formData) => {
    console.log(formData);
  };
....
        <Form validateTrigger={["onBlur"]} onFinish={onFinish}>
          ....
          <Form.Item>
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
        </Form>
     ....

export default Login;

4. aixos二次封装

因为需要向后端发起请求,涉及token认证的地方需要设置请求拦截器,也可能需要设置响应拦截器,所以需要对axios二次封装

在此之前,我曾详细记录过如何使用react+redux完成登录页面及token的存取和登录保持,因此,整个登录不再赘述,只上关键过程和重要代码

实现步骤

  1. 安装 axios 到项目
  2. 创建 utils/http.jsx 文件
  3. 创建 axios 实例,配置 baseURL,请求拦截器,响应拦截器

https.jsx

import axios from "axios";

const http = axios.create({
  baseURL: "http://geek.itheima.net/v1_0",
  timeout: 5000,
});

// axios请求拦截器
http.interceptors.request.use(
  (config) => {
    return config;
  },
  (e) => Promise.reject(e)
);

// axios响应式拦截器
http.interceptors.response.use(
  (res) => res.data,
  (e) => {
    console.log(e);
    return Promise.reject(e);
  }
);

export default http;

5. 引入redux管理全局数据

react中的redux就相当于vue中的vuex,都用于管理全局数据,登录时后端返回的token数据就是全局需要的数据

实现步骤

  1. 安装redux相关包

    npm i react-redux @reduxjs/toolkit
    
  2. 配置redux,配置redux在另一篇博客中有详细记录,不再具体说明

    1. 新建user模块store/moduls/user.jsx,填入以下代码
    import { createSlice } from "@reduxjs/toolkit";
    import http from "../../utils/http";
    const userStore = createSlice({
      name: "user",
      // 数据状态
      initialState: {
        token: "",
      },
      // 同步修改方法
      reducers: {
        setToken(state, action) {
          state.userInfo = action.payload;
        },
      },
    });
    
    // 解构出actionCreater
    const { setToken } = userStore.actions;
    
    // 获取reducer函数
    const userReducer = userStore.reducer;
    
    // 异步方法封装
    const fetchLogin = (loginForm) => {
      return async (dispatch) => {
        const res = await http.post("/authorizations", loginForm);
        dispatch(setToken(res.data.token));
      };
    };
    
    export { fetchLogin };
    
    export default userReducer;
    
    
    1. 在index.jsx中注册子模块,store/index.jsx
    import { configureStore } from "@reduxjs/toolkit";
    import userReducer from "./modules/user";
    
    export default configureStore({
        reducer: {
            user: userReducer
        }
    })
    
    1. 入口文件中全局注册store,main.jsx
    import ReactDOM from "react-dom/client";
    import App from "./App.jsx";
    import "./index.scss";
    import { RouterProvider } from "react-router-dom";
    import router from "./router/index.jsx";
    import { Provider } from "react-redux";
    import store from "./store/index.jsx";
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <Provider store={store}>
        <RouterProvider router={router} />
      </Provider>
    );
    
    

6. 实现登录逻辑

实现步骤

  1. 收集表单信息,向后端发送登录请求
  2. 登录成功后跳转到首页,提示用户登录成功

主要是修改上面的Login/index.jsx中的onFinish方法

如下:

// 省略其他代码
// .......
import { useDispatch } from "react-redux";
import { fetchLogin } from "../../store/modules/user";
import { useNavigate } from "react-router-dom";

// 省略其他代码
// .......
  const onFinish = async (formData) => {
    console.log(formData);
    await dispatch(fetchLogin(formData))
    navigate('/')
    message.success('登录成功')
  };
// 省略其他代码
// .......

7. 实现token持久化存储

其实就是登录时把token存到localstorage中去,react+redux完成登录页面及token的存取和登录保持–这篇博客中详细记录了,这里只上关键代码

  1. 首先封装token的存、取、删方法,utils/token.jsx
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 };

  1. localstorage中持久化存储token,逻辑就是在redux的同步方法中,存储token,同时,token的初始化不再是空值,当localstorage中有token时,就取出来,没有就是空值

    store/moduls/user.jsx

import { createSlice } from "@reduxjs/toolkit";
import http from "../../utils/http";
import { setToken as _setToken, getToken } from "../../utils/token";
const userStore = createSlice({
  name: "user",
  // 数据状态
  initialState: {
    // 差异1
    token: getToken() || "",
  },
  // 同步修改方法
  reducers: {
     setToken(state, action) {
      state.token = action.payload;
      // 存入本地
      _setToken(state.token);
    },
  },
});

8. 请求拦截器中携带token

常规操作,在axios二次封装的http.jsx文件中添加以下代码

// axios请求拦截器
http.interceptors.request.use(
  (config) => {
    // 导入getToken方法
    const token = getToken()
    if (token) {
      // 请求头携带token
      config.headers.Authorization = "Bearer " + token;
    }
    return config;
  },
  (e) => Promise.reject(e)
);

9. 路由守卫

vue中的路由守卫是在router中实现的,react的做法是封装 AuthRoute 路由鉴权高阶组件,然后将需要鉴权的页面路由配置,替换为 AuthRoute 组件渲染

实现步骤

  1. 在 components 目录中,创建 AuthRoute/index.jsx 文件
  2. 登录时,直接渲染相应页面组件
  3. 未登录时,重定向到登录页面
  4. 将需要鉴权的页面路由配置,替换为 AuthRoute 组件渲染

AuthRoute/index.jsx中的代码

import { getToken } from '../../utils/token'
import { Navigate } from 'react-router-dom'

const AuthRoute = ({ children }) => {
  const isToken = getToken()
  if (isToken) {
    return <>{children}</>
  } else {
    return <Navigate to="/login" replace />
  }
}

export default AuthRoute

Layout页面需要鉴权,所以在路由中修改页面的渲染配置,router/index.jsx

import { createBrowserRouter } from "react-router-dom";

import Login from "../pages/Login";
import Layout from "../pages/Layout";
import AuthRoute from "../components/AuthRoute";

const router = createBrowserRouter([
  {
    path: "/",
    element: <AuthRoute><Layout /></AuthRoute>,
  },
  {
    path: "/login",
    element: <Login />,
  },
]);

export default router;

10. 封装接口调用的api

因为后面涉及多个后端接口调用,所以好的做法是把后端接口进行统一的封装

新建apis/user.jsx文件,用于处理用户相关的接口

import http from "../utils/http";

export const loginAPI = (data) => {
  return http({
    url: "/authorizations",
    method: "POST",
    data,
  });
};

把前面的第5节中redux异步方法请求改写一下

// 异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    const res = await loginAPI(loginForm); // 注意loginAPI要引入
    dispatch(setToken(res.data.token));
  };
};

后面其他api也就抽离出来了

三、Layout首页设计

1. 搭建首页基础框架

首页的基础框架长下面这个样子

image-20240328161156442

先填入基础代码

import React, { useEffect, useState } from "react";
import "./index.scss";
import {
  HomeOutlined,
  DiffOutlined,
  EditOutlined,
  LogoutOutlined,
} from "@ant-design/icons";
import { Breadcrumb, Layout, Menu, theme, Popconfirm } from "antd";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
// import { fetchUserInfo, clearUserInfo } from "../../store/modules/user";
const { Header, Content, Sider } = Layout;

const items = [
  {
    label: "首页",
    key: "/",
    icon: <HomeOutlined />,
  },
  {
    label: "文章管理",
    key: "/article",
    icon: <DiffOutlined />,
  },
  {
    label: "创建文章",
    key: "/publish",
    icon: <EditOutlined />,
  },
];

const GLayout = () => {
  const [collapsed, setCollapsed] = useState(false);
  const {
    token: { colorBgContainer, borderRadiusLG },
  } = theme.useToken();
  return (
    <Layout
      style={{
        minHeight: "100vh",
      }}
    >
      <Sider
        theme="light"
        collapsible
        collapsed={collapsed}
        onCollapse={(value) => setCollapsed(value)}
      >
        <div className="demo-logo-vertical" />
        <Menu
          //   theme="dark"
            defaultSelectedKeys={["/"]}
        //   selectedKeys={selectedKey}
          mode="inline"
          items={items}
        //   onClick={clickMenu}
        />
      </Sider>
      <Layout>
        <Header
          className="header"
          style={{
            padding: 0,
            background: colorBgContainer,
          }}
        >
          <div className="logo"></div>
          <div className="user-info">
            <span className="user-name">React</span>
            <span className="user-logout">
              <Popconfirm
                title="是否确认退出?"
                okText="退出"
                cancelText="取消"
                // onConfirm={logout}
              >
                <LogoutOutlined /> 退出
              </Popconfirm>
            </span>
          </div>
        </Header>
        <Content
          className="content"
          style={{
            margin: "5px 5px",
            background: colorBgContainer,
            borderRadius: borderRadiusLG,
          }}
        >
          <Outlet />
        </Content>
      </Layout>
    </Layout>
  );
};
export default GLayout;

补充对应的样式

.header {
    display: flex;
    justify-content: space-between;
    align-items: center;

    .logo {
        width: 200px;
        height: 60px;
        background: url('../../assets/global.png') no-repeat center / 160px auto;
    }

    .user-info {
       margin-right: 20px;
        color: #070707;

        .user-name {
            margin-right: 20px;
        }

        .user-logout {
            display: inline-block;
            cursor: pointer;
        }
    }
}

.content {
    height: 100%;
    
}

2. 配置二级路由

就是把左侧的文章管理、创建文章和首页的路由给配置出来

使用步骤

  1. 在 pages 目录中,分别创建:Home(数据概览)/Article(内容管理)/Publish(发布文章)页面文件夹

  2. 分别在三个文件夹中创建 index.jsx 并创建基础组件后导出

  3. router/index.js 中配置嵌套子路由,在Layout中配置二级路由出口

    import { createBrowserRouter } from "react-router-dom";
    import Layout from "../pages/Layout";
    import Login from "../pages/Login";
    import AuthRoute from "../components/AuthRoute";
    import Home from "../pages/Home";
    import Article from "../pages/Article";
    import Publish from "../pages/Publish";
    
    const router = createBrowserRouter([
      {
        path: "/",
        element: <AuthRoute><Layout /></AuthRoute>,
        children: [
          {
            // path: 'home',
            index: true,
            element: <Home />
          },
          {
            path: 'article',
            element: <Article />
          },
          {
            path: 'publish',
            element: <Publish />
          },
        ]
      },
      {
        path: "/login",
        element: <Login />,
      },
    ]);
    export default router;
    
    
  4. 使用 Link 修改左侧菜单内容,与子路由规则匹配实现路由切换,前面提供的代码中已经配置好了,就是<Outlet />

3. 点击菜单跳转至对应的二级路由

  1. 在menu菜单中添加点击回调函数

image-20240328162519204

const navigate = useNavigate();
  const clickMenu = (route) => {
    navigate(route.key);
  }
  1. 菜单反向高亮

    是个啥意思勒,目前点击菜单,菜单栏是高亮的,但是如果冲地址栏直接输入地址,对应的菜单并不能高亮。。

    image-20240328162816466

我在使用elementplus时经常遇到这个问题,一度以为是框架的bug,现在才搞明白,原来是自己没有处理好,处理逻辑是先获取页面当前的地址,然后把menu中的selectedKeys属性设置为当前地址

const GLayout = () => {
  // 省略部分代码
  // 获取当前页面地址-----------------1
  const location = useLocation()
  const selectedKey = location.pathname
  
  return (
    <Layout>
      <Header className="header">
        <div className="logo" />
        <div className="user-info">
          <span className="user-name">{name}</span>
          <span className="user-logout">
            <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
              <LogoutOutlined /> 退出
            </Popconfirm>
          </span>
        </div>
      </Header>
      <Layout>
        <Sider width={200} className="site-layout-background">
          <Menu
            mode="inline"
            theme="dark"
			// 将当前页面地址设置为selectedKeys----------------2
            selectedKeys={selectedKey}
            items={items}
            style={{ height: '100%', borderRight: 0 }}
            onClick={menuClickHandler}></Menu>
        </Sider>
        <Layout className="layout-content" style={{ padding: 20 }}>
          <Outlet />
        </Layout>
      </Layout>
    </Layout>
  )
}

4. 头部导航栏显示个人信息

这部分其实应该可以直接登录的时候就直接给了,写在user的store中,不过黑马提供的逻辑是重新调了一个接口,这个接口返回的才是用户信息。别人怎么提供就怎么来吧

实现步骤

  1. 编写获取个人信息的接口

    export const getProfileAPI = () => {
      return http({
        url: "/user/profile",
      });
    };
    
  2. 在Redux的store中编写获取用户信息的相关逻辑

    // 异步方法,获取用户个人信息
    const fetchUserInfo = () => {
      return async (dispatch) => {
        try {
          const res = await getProfileAPI();
          // console.log(res)
          dispatch(setUserInfo(res.data));
        } catch (error) {
          message.error("登录信息失效,请重新登录");
        }
      };
    };
    

    要把这个方法暴露出去

  3. 在Layout组件中触发action的执行

  4. 在Layout组件使用使用store中的数据进行用户名的渲染

    以上两步代码如下:

    import { fetchUserInfo } from "../../store/modules/user";
      //   获取用户信息
      const dispatch = useDispatch();
      useEffect(() => {
        dispatch(fetchUserInfo());
      }, []);
    
      const { userInfo } = useSelector((state) => state.user);
    <span className="user-name">{userInfo.name}</span>
    

5. 退出登录逻辑

也是常规操作,之前是在pinia中写一个删除store的方法,是个同步方法,redux中差不多

实现步骤

  1. 为气泡确认框添加确认回调事件,实际上就是onConfirm事件

                  <Popconfirm
                    title="是否确认退出?"
                    okText="退出"
                    cancelText="取消"
                    onConfirm={logout}
                  >
                    <LogoutOutlined /> 退出
                  </Popconfirm>
    
  2. store/userStore.jsx 中新增退出登录的action函数,在其中删除token

    // 同步修改方法
      reducers: {
        ........
        clearUserInfo(state) {
          state.token = "";
          state.userInfo = "";
          clearToken();
        },
    

    注意对外暴露出去

  3. 在回调事件中,调用userStore中的退出action

  4. 清除用户信息,返回登录页面

    const logout = () => {
        dispatch(clearUserInfo())
        navigate("/login");
    }
    

6. 处理token失效

一般是token过期后的处理逻辑,后端会返回401代码,响应拦截器中根据这个代码进行路由跳转至登录页面

utils/http.jsx中响应拦截器添加如下代码

import router from "../router";

// axios响应式拦截器
http.interceptors.response.use(
  (res) => res.data,
  (e) => {
    console.log(e);
    // 401 -- token失效
    if(e.response.status === 401){
      clearToken()
      // router实例
      router.navigate('/login')
    }
    return Promise.reject(e);
  }
);

export default http;

7. 首页绘制echarts图

echarts图我在vue中画过无数遍了,这里的逻辑基本上一样

  1. 首先安装echarts
npm i echarts
  1. 封装一个画图组件

    在Home目录下新建components目录,并创建BarChart.jsx组件

    封装的代码如下:

    import * as echarts from "echarts";
    import { useEffect, useRef } from "react";
    // 父子组件通讯props
    const BarChart = ({
      title,
      xData,
      sData,
      style = { width: "400px", height: "300px" },
    }) => {
      const chartRef = useRef(null);
      let initChart;
      const drawChart = () => {
        if (initChart != null && initChart != "" && initChart != undefined) {
          initChart.dispose(); //销毁
        }
        initChart = echarts.init(chartRef.current);
        const option = {
          title: {
            text: title,
          },
          xAxis: {
            type: "category",
            data: xData,
          },
          yAxis: {
            type: "value",
          },
          series: [
            {
              data: sData,
              type: "bar",
            },
          ],
        };
        initChart.setOption(option);
        window.addEventListener("resize", () => {
          initChart.resize();
        });
      };
      useEffect(() => drawChart(), [xData, sData]);
      return (
        <>
          <div ref={chartRef} style={style}></div>
        </>
      );
    };
    export default BarChart;
    
    

    几个要点记录一下:

    • BarChart是子组件,父组件应传递 title, xData, sData, style 这几个属性
    • react中获取dom是用的react中useRef钩子,vue中直接就是ref
    • useEffect需要监听数据变化,然后重绘图
  2. Home组件中调用子组件,并传递子组件所需的数据

    import BarChart from "./components/BarChart";
    
    const Home = () => {
      return (
        <>
          <BarChart
            xData={["Vue", "React", "Angular"]}
            sData={[2000, 5000, 1000]}
            title={"三大框架使用率"}
          ></BarChart>
          <BarChart
            xData={["Vue", "React", "Angular"]}
            sData={[200, 500, 100]}
            title={"三大框架满意度"}
            style={{ width: "500px", height: "400px" }}
          ></BarChart>
        </>
      );
    };
    
    export default Home;
    
    

最终展示效果

image-20240328171620554

四、发布文章模块

就是下面这个页面

image-20240328172027729

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'

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;

    .publish-quill {
        .ql-editor {
            min-height: 300px;
        }
    }
}

.ant-upload-list {

    .ant-upload-list-picture-card-container,
    .ant-upload-select {
        width: 146px;
        height: 146px;
        background: #eee;
    }
}

2. 添加富文本编辑器

实现步骤

  1. 安装富文本编辑器

    npm i react-quill@2.0.0-beta.2
    

    这里可能会报错,应该改成

    npm i react-quill@2.0.0-beta.2 --force
    
  2. 导入富文本编辑器组件以及样式文件

  3. 渲染富文本编辑器组件

  4. 调整富文本编辑器的样式

    publish.jsx中的代码

    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="请输入内容"
                ></ReactQuill>
              </Form.Item>
    
              <Form.Item wrapperCol={{ offset: 4 }}>
                <Space>
                  <Button size="large" type="primary" htmlType="submit">
                    发布文章
                  </Button>
                </Space>
              </Form.Item>
            </Form>
          </Card>
        </div>
      );
    };
    
    export default Publish;
    
    

3. antD中的select组件获取频道数据

image.png

这个数据是从后端获取的

实现步骤

  1. 使用useState初始化数据和修改数据的方法

      const [channels, setChannels] = useState([]);
    
  2. 在useEffect中调用接口并保存数据

    封装接口代码,apis目录新建article.jsx文件,填写接口请求函数

    import http from "../utils/http";
    
    export const getChannelAPI = () => {
      return http({
        url: "/channels",
      });
    };
    

    Publish.jsx编写请求数据的函数,并在副作用钩子中调用

      const fetchChannels = async () => {
        const res = await getChannelAPI();
        console.log(res);
        setChannels(res.data.channels);
      };
      useEffect(() => {
        fetchChannels();
      }, []);
    
  3. 使用数据渲染对应模版

                <Select placeholder="请选择文章频道" style={{ width: 400 }}>
                  {channels.map((item) => (
                    <Option value={item.id} key={item.id}>
                      {item.name}
                    </Option>
                  ))}
                </Select>
    

4. 发布文章

  1. 先封装发布文章的接口

    // 新增
    export const publishAPI = (data) => {
      return http({
        url: "/mp/articles?draft=false",
        method: "POST",
        data,
      });
    };
    
  2. form提交submit的onFinish回调函数

      const onFinish = async (values) => {
        const { channel_id, content, title } = values;
        const data = {
          channel_id,
          content,
          title,
          type: 1,
          cover: {
            type: 1,
            images: [],
          },
        };
        await publishAPI(data)
      };
    

apis目录新建article.jsx文件,填写接口请求函数

import http from "../utils/http";

export const getChannelAPI = () => {
  return http({
    url: "/channels",
  });
};

Publish.jsx编写请求数据的函数,并在副作用钩子中调用

  const fetchChannels = async () => {
    const res = await getChannelAPI();
    console.log(res);
    setChannels(res.data.channels);
  };
  useEffect(() => {
    fetchChannels();
  }, []);
  1. 使用数据渲染对应模版

                <Select placeholder="请选择文章频道" style={{ width: 400 }}>
                  {channels.map((item) => (
                    <Option value={item.id} key={item.id}>
                      {item.name}
                    </Option>
                  ))}
                </Select>
    

4. 发布文章

  1. 先封装发布文章的接口

    // 新增
    export const publishAPI = (data) => {
      return http({
        url: "/mp/articles?draft=false",
        method: "POST",
        data,
      });
    };
    
  2. form提交submit的onFinish回调函数

      const onFinish = async (values) => {
        const { channel_id, content, title } = values;
        const data = {
          channel_id,
          content,
          title,
          type: 1,
          cover: {
            type: 1,
            images: [],
          },
        };
        await publishAPI(data)
      };
    

未完待续,后面再补充吧~~~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软件工程学习笔记12——运行维护篇

运行维护篇 一、版本发布1、关于软件版本2、版本发布前&#xff0c;做好版本发布的规划3、规范好发布流程&#xff0c;保障发布质量 二、DevOps工程师1、什么是 DevOps 三、线上故障1、遇到线上故障&#xff0c;新手和高手的差距在哪里2、大厂都是怎么处理线上故障的 四、日志管…

自然语言处理(NLP)全面指南

自然语言处理&#xff08;NLP&#xff09;是人工智能领域中最热门的技术之一&#xff0c;它通过构建能够理解和生成人类语言的机器&#xff0c;正在不断推动技术的发展。本文将为您提供NLP的全面介绍&#xff0c;包括其定义、重要性、应用场景、工作原理以及面临的挑战和争议。…

JavaEE之网络初识(网络中的一些基本概念)详解

&#x1f63d;博主CSDN主页: 小源_&#x1f63d; &#x1f58b;️个人专栏: JavaEE &#x1f600;努力追逐大佬们的步伐~ 目录 1. 前言 2. 网络中的一些基本概念 2.1 IP地址 2.2 端口号 2.3 网络协议 2.4 协议分层 2.5 封装 2.6 分用 (封装的逆向过程) 2.7 客户端 vs …

uniApp使用XR-Frame创建3D场景(7)加入点击交互

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用 这篇我们讲解如何与场景中的模型交互&#xff08;点击识别&#xff09; 先看源码 <xr-scene render-system"alpha:true" bind:ready"handleReady"><xr-node><xr-mesh id"…

连续信号离散信号的功率谱密度--用MATLAB求功率谱密度

连续信号&离散信号的功率谱密度--用MATLAB求功率谱密度 目录 前言 一、能量及功率定义 1、连续信号 2、离散信号 二、功率谱密度计算公式 三、MATLAB仿真 1、源代码 2、仿真结果分析 总结 前言 一直对数字信号处理中的功率谱密度计算有点好奇&#xff0c;虽然MATLAB有提供现…

【MySQL】15. 事务管理(重点) -- 1

1. CURD不加控制&#xff0c;会有什么问题&#xff1f; 2. CURD满足什么属性&#xff0c;能解决上述问题&#xff1f; 买票的过程得是原子的 ?买票互相应该不能影响 ?买完票应该要永久有效 ?买前&#xff0c;和买后都要是确定的状态? 3. 什么是事务&#xff1f; 事务就是…

探索 2024 年 Web 开发最佳前端框架

前端框架通过简化和结构化的网站开发过程改变了 Web 开发人员设计和实现用户界面的方法。随着 Web 应用程序变得越来越复杂&#xff0c;交互和动画功能越来越多&#xff0c;这是开发前端框架的初衷之一。 在网络的早期&#xff0c;网页相当简单。它们主要以静态 HTML 为特色&a…

CSS 结构伪类选择器 伪元素选择器 盒子模型

目录 1. 结构伪类选择器1.1 :nth-child(公式) 2. 伪元素选择器3. 盒子模型3.1 盒子模型的重要组成部分3.2 盒子模型 - 边框线3.3 盒子模型 - 内边距3.4 盒子模型 - 尺寸计算3.5 盒子模型 - 外边距3.6 盒子模型 - 元素溢出3.7 外边距问题 - 合并现象3.8 外边距问题 - 塌陷问题3.…

游戏推广的新篇章:Xinstall助力实现全渠道效果统计与提升

随着游戏市场的日益繁荣&#xff0c;游戏推广已成为各大游戏公司争夺市场份额的关键环节。然而&#xff0c;面对众多推广渠道和复杂的用户行为&#xff0c;如何精准地评估推广效果、优化投放策略&#xff0c;成为了游戏推广人员亟待解决的问题。此时&#xff0c;Xinstall作为一…

如何使用Windows电脑部署Lychee私有图床网站并实现无公网IP远程管理本地图片

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-MSVdVLkQMnY9Y2HW {font-family:"trebuchet ms",verdana,arial,sans-serif;f…

网络工程师实验命令(华为数通HCIA)

VRP系统的基本操作 dis version #查看设备版本信息 sys #进入系统视图 system-name R1 #改设备名字为R1进入接口配置IP地址 int g0/0/0 ip address 192.168.1.1 255.255.255.0 #配置接口地址为192.168.1.1/255.255.255.0 ip address 192.168.1.2 24 sub #此…

flutter生成二维码并截图保存到图库

引入库&#xff1a;flutter_screenutil、image_gallery_saver、qr_flutter弹窗布局 import dart:async; import dart:typed_data; import package/generated/l10n.dart; import package:jade/configs/PathConfig.dart; import package:jade/utils/ImageWaterMarkUtil.dart; im…

车载以太网AVB交换机 gptp透明时钟 8口 千兆/百兆可切换 SW1100TR

SW1100车载以太网交换机 一、产品简要分析 8端口千兆和百兆混合车载以太网交换机&#xff0c;其中包含2个通道的1000BASE-T1采用罗森博格H-MTD接口&#xff0c;5通道100BASE-T1泰科MATEnet接口和1个通道1000BASE-T标准以太网(RJ45接口)&#xff0c;可以实现车载以太网多通道交…

Flink系列之:Flink SQL Gateway

Flink系列之&#xff1a;Flink SQL Gateway 一、Flink SQL Gateway二、部署三、启动SQL Gateway四、运行 SQL 查询五、SQL 网关启动选项六、SQL网关配置七、支持的端点 一、Flink SQL Gateway SQL 网关是一项允许多个客户端从远程并发执行 SQL 的服务。它提供了一种简单的方法…

Unity图集编辑器

图集编辑器 欢迎使用图集编辑器新的改变编辑器图片 欢迎使用图集编辑器 Unity图集操作很是费劲 无法批量删除和添加图集中的图片 新的改变 自己写了一个图集编辑器 客&#xff1a; 支持批量删除 左键点击图片代表选中 右键点击图标定位到资产支持批量添加 选中图片拖拽到编…

Linux 系统 部署weblogic(新手版)

Linux 系统 部署weblogic&#xff08;新手版&#xff09; 一、 1、如果原环境有jdk则需要卸载。 先用命令查看 rpm -qa|grep java 如果有jdk则需要卸载rpm -e --nodeps java-1.7.0-openjdk-1.7.0.191-2.6.15.5.el7.x86_64rpm -e --nodeps java-1.8.0-openjdk-headl…

京东云0基础搭建帕鲁服务器_4核16G和8核32G幻兽帕鲁专用服务器

使用京东云服务器搭建幻兽帕鲁Palworld游戏联机服务器教程&#xff0c;非常简单&#xff0c;京东云推出幻兽帕鲁镜像系统&#xff0c;镜像直接选择幻兽帕鲁镜像即可一键自动部署&#xff0c;不需要手动操作&#xff0c;真正的新手0基础部署幻兽帕鲁&#xff0c;阿腾云atengyun.…

【Java面试题】Redis上篇(基础、持久化、底层数据结构)

文章目录 基础1.什么是Redis?2.Redis可以用来干什么&#xff1f;3.Redis的五种基本数据结构&#xff1f;4.Redis为什么这么快&#xff1f;5.什么是I/O多路复用&#xff1f;6.Redis6.0为什么使用了多线程&#xff1f; 持久化7.Redis的持久化方式&#xff1f;区别&#xff1f;8.…

【JavaEE】初识线程,线程与进程的区别

文章目录 ✍线程是什么&#xff1f;✍线程和进程的区别✍线程的创建1.继承 Thread 类2.实现Runnable接口3.匿名内部类4.匿名内部类创建 Runnable ⼦类对象5.lambda 表达式创建 Runnable ⼦类对象 ✍线程是什么&#xff1f; ⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按…

Github 2024-03-28Go开源项目日报Top10

根据Github Trendings的统计,今日(2024-03-28统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Go项目9非开发语言项目1Ollama: 本地大型语言模型设置与运行 创建周期:248 天开发语言:Go协议类型:MIT LicenseStar数量:42421 个Fork数量:…