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-20240329084425524

  1. 首页

image-20240328115845338

  1. 文章管理

image-20240328115913121

  1. 创建文章

image-20240328115938645

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

〇、代码仓库地址及视频地址

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

react+vite+redux+echarts前端练手小项目

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

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.Itemname="mobile"rules={[{ required: true, message: '请输入手机号' },{pattern: /^1[3-9]\d{9}$/,message: '手机号码格式不对'}]}><Input size="large" placeholder="请输入手机号" /></Form.Item><Form.Itemname="code"rules={[{ required: true, message: '请输入验证码' },]}><Input size="large" placeholder="请输入验证码" maxLength={6} /></Form.Item><Form.Item><Button type="primary" htmlType="submit" size="large" block>登录</Button></Form.Item></Form>)
}

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: {// 差异1token: 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) {// 请求头携带tokenconfig.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 (<Layoutstyle={{minHeight: "100vh",}}><Sidertheme="light"collapsiblecollapsed={collapsed}onCollapse={(value) => setCollapsed(value)}><div className="demo-logo-vertical" /><Menu//   theme="dark"defaultSelectedKeys={["/"]}//   selectedKeys={selectedKey}mode="inline"items={items}//   onClick={clickMenu}/></Sider><Layout><HeaderclassName="header"style={{padding: 0,background: colorBgContainer,}}><div className="logo"></div><div className="user-info"><span className="user-name">React</span><span className="user-logout"><Popconfirmtitle="是否确认退出?"okText="退出"cancelText="取消"// onConfirm={logout}><LogoutOutlined /> 退出</Popconfirm></span></div></Header><ContentclassName="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 = () => {// 省略部分代码// 获取当前页面地址-----------------1const location = useLocation()const selectedKey = location.pathnamereturn (<Layout><Header className="header"><div className="logo" /><div className="user-info"><span className="user-name">{name}</span><span className="user-logout"><Popconfirm title="是否确认退出?" okText="退出" cancelText="取消"><LogoutOutlined /> 退出</Popconfirm></span></div></Header><Layout><Sider width={200} className="site-layout-background"><Menumode="inline"theme="dark"// 将当前页面地址设置为selectedKeys----------------2selectedKeys={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事件

                  <Popconfirmtitle="是否确认退出?"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 (<><BarChartxData={["Vue", "React", "Angular"]}sData={[2000, 5000, 1000]}title={"三大框架使用率"}></BarChart><BarChartxData={["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 } = Selectconst Publish = () => {return (<div className="publish"><Cardtitle={<Breadcrumb items={[{ title: <Link to={'/'}>首页</Link> },{ title: '发布文章' },]}/>}><FormlabelCol={{ span: 4 }}wrapperCol={{ span: 16 }}initialValues={{ type: 1 }}><Form.Itemlabel="标题"name="title"rules={[{ required: true, message: '请输入文章标题' }]}><Input placeholder="请输入文章标题" style={{ width: 400 }} /></Form.Item><Form.Itemlabel="频道"name="channel_id"rules={[{ required: true, message: '请选择文章频道' }]}><Select placeholder="请选择文章频道" style={{ width: 400 }}><Option value={0}>推荐</Option></Select></Form.Item><Form.Itemlabel="内容"name="content"rules={[{ required: true, message: '请输入文章内容' }]}></Form.Item><Form.Item wrapperCol={{ offset: 4 }}><Space><Button size="large" type="primary" htmlType="submit">发布文章</Button></Space></Form.Item></Form></Card></div>)
}export default Publish

样式文件

.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"><Cardtitle={<Breadcrumbitems={[{ title: <Link to={"/"}>首页</Link> },{ title: "发布文章" },]}/>}><FormlabelCol={{ span: 4 }}wrapperCol={{ span: 16 }}initialValues={{ type: 1 }}><Form.Itemlabel="标题"name="title"rules={[{ required: true, message: "请输入文章标题" }]}><Input placeholder="请输入文章标题" style={{ width: 400 }} /></Form.Item><Form.Itemlabel="频道"name="channel_id"rules={[{ required: true, message: "请选择文章频道" }]}><Select placeholder="请选择文章频道" style={{ width: 400 }}><Option value={0}>推荐</Option></Select></Form.Item><Form.Itemlabel="内容"name="content"rules={[{ required: true, message: "请输入文章内容" }]}><ReactQuillclassName="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)};
    

5. 文章封面图片上传逻辑

(1)封面上传结构

在频道和内容之间插入如下代码

<Form.Item label="封面"><Form.Item name="type"><Radio.Group><Radio value={1}>单图</Radio><Radio value={3}>三图</Radio><Radio value={0}>无图</Radio></Radio.Group></Form.Item><UploadlistType="picture-card"showUploadList><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>
</Form.Item>
(2)实现基础上传功能

实现步骤

  1. 为 Upload 组件添加 action 属性,配置封面图片上传接口地址

          <Uploadname="image"listType="picture-card"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}onChange={onUploadChange}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>
    
  2. 为 Upload组件添加 name属性, 接口要求的字段名

  3. 为 Upload 添加 onChange 属性,在事件中拿到当前图片数据,并存储到图片列表中

      //   上传图片的回调const [imageList, setImageList] = useState([]);const onUploadChange = (info) => {setImageList(info.fileList);};
    
(3)切换图片选项

其实就是单图、无图和三图的切换

实现步骤

  1. 点击单选框时拿到当前的类型value
  2. 根据value控制上传组件的显示(大于零时才显示)
............
//   radio选择切换回调const [imageType, setImageType] = useState(1);const onTypeChange = (value) => {setImageType(value.target.value);};
............<Form.Item label="封面"><Form.Item name="type" onChange={onTypeChange}><Radio.Group><Radio value={1}>单图</Radio><Radio value={3}>三图</Radio><Radio value={0}>无图</Radio></Radio.Group></Form.Item>{imageType > 0 && (<Uploadname="image"listType="picture-card"showUploadListaction={"http://geek.itheima.net/v1_0/upload"}onChange={onUploadChange}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>)}</Form.Item>
...........
(4)控制图片数量选择

upload设置maxCount 属性限制图片的上传图片数量

并通multiple属性确定是否允许多选图片

			<Uploadname="image"listType="picture-card"showUploadListaction={"http://geek.itheima.net/v1_0/upload"}onChange={onUploadChange}maxCount={imageType}multiple={imageType > 1}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>
(5)发布带封面的文章

需要重写提交的onFinish方法

实现步骤

  1. 校验图片类型和数量是否吻合
  2. 处理图片列表为接口所需的数据格式
  const onFinish = async (values) => {if (imageType !== imageList.length)return message.warning("图片类型和数量不一致");const { channel_id, content, title } = formValue;const data = {channel_id,content,title,type: imageType,cover: {type: imageType,images: imageList.map((item) => item.response.data.url),},};await publishAPI(data);message.success("发布成功");navigate("/article");};

这里可能会有报错,就是传3图的时候,imageType='3’是字符串,而传单图时,imageType=1却是整数,我第一遍写的时候没有问题,后来传3图却怎么也传不上去,如果有问题,转换一下imageType的类型就行了

五、文章列表管理

分成两块,上面是文章筛选,下面是筛选的文章列表,如下:

image-20240329084718903

1. 创建页面静态结构

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 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 />} /><Buttontype="primary"dangershape="circle"icon={<DeleteOutlined />}/></Space>);},},];// 准备表格body数据const data = [{id: "8218",comment_count: 0,cover: {images: [],},like_count: 0,pubdate: "2019-03-11 09:00:00",read_count: 2,status: 2,title: "wkwebview离线化加载h5资源解决方案",},];return (<div><Cardtitle={<Breadcrumbitems={[{ title: <Link to={"/"}>首页</Link> },{ title: "文章列表" },]}/>}style={{ marginBottom: 20 }}><Form initialValues={{ status: "" }}><Form.Item label="状态" name="status"><Radio.Group><Radio value={""}>全部</Radio><Radio value={0}>草稿</Radio><Radio value={2}>审核通过</Radio></Radio.Group></Form.Item><Form.Item label="频道" name="channel_id"><Selectplaceholder="请选择文章频道"defaultValue="lucy"style={{ width: 120 }}><Option value="jack">Jack</Option><Option value="lucy">Lucy</Option></Select></Form.Item><Form.Item label="日期" name="date">{/* 传入locale属性 控制中文显示*/}<RangePicker locale={locale}></RangePicker></Form.Item><Form.Item><Button type="primary" htmlType="submit" style={{ marginLeft: 40 }}>筛选</Button></Form.Item></Form></Card><Card title={`根据筛选条件共查询到 count 条结果:`}><Table rowKey="id" columns={columns} dataSource={data} /></Card></div>);
};export default Article;

2. 渲染筛选form中的频道数据

前面发布文章已经做过了,掉下接口,然后渲染即可

import { useEffect, useState } from "react";
import { getChannelAPI } from "../../apis/article";............const [channels, setChannels] = useState([]);const getChannel = async () => {const res = await getChannelAPI();setChannels(res.data.channels);};useEffect(() => {getChannel();}, []);.............<Selectplaceholder="请选择文章频道"style={{ width: 120 }}>{channels.map((item) => (<Option value={item.id} key={item.id}>{item.name}</Option>))}</Select>

3. 渲染表格区域数据

实现步骤

  1. 编写获取数据的接口

    export const getArticleListAPI = (params) => {return http({url: "/mp/articles",params,});
    };
    

    注意这个接口是需要传参的,在调用接口时我再描述一下参数的结构

  2. 声明列表相关数据管理

  3. 使用useState声明参数相关数据管理

  4. 调用接口获取数据

  5. 使用接口数据渲染模板

页面上渲染数据的代码

.............
import { getChannelAPI, getArticleListAPI } from "../../apis/article";
const Article = () => {// 筛选条件,也就是接口参数const [reqData, setReqData] = useState({status: "", // 文章审核状态channel_id: "", // 频道idbegain_pubdate: "", // 开始日期end_pubdate: "", // 结束日期page: 1, // 页码per_page: 4, // 每页显示数据});// 表格数据const [list, setList] = useState([]);// 数据总条数const [count, setCount] = useState(0);const getList = async () => {const res = await getArticleListAPI(reqData);setList(res.data.results);setCount(res.data.total_count);};useEffect(() => {getChannel();getList();}, [reqData]);return (............<Card title={`根据筛选条件共查询到 ${count} 条结果:`}><Table rowKey="id" columns={columns} dataSource={list} /></Card>)
}

以上代码完成了接口参数的初始化、表格数据的初始化和获取、数据总条数的初始化和获取,并在副作用函数中调用相关数据的获取方法

4. 表格筛选功能实现

点击筛选按钮,表格区域的数据应该能改变

实现步骤

  1. 为表单添加onFinish属性监听表单提交事件,获取参数
  2. 根据接口字段格式要求格式化参数格式
  3. 修改params 参数并重新使用新参数重新请求数据

代码实现

  // 筛选功能实现const onFinish = (formData) => {setReqData({...reqData,channel_id: formData.channel_id,status: formData.status,begain_pubdate: formData.date[0].format("YYYY-MM-DD"),end_pubdate: formData.date[1].format("YYYY-MM-DD"),});};

注意,这个onFinish其实仅仅改变了reqData中的数据,其实就是把reqData中部分字段改成了筛选选中的值,这样就能筛选数据了吗?

的确,因为我在副作用函数useEffect中监听了reqData,只要reqData发生变化,getList()就会重新执行

5. 表格分页功能实现

其实在上述reqData,已经设置了分页的参数,就是page和per_page: 4,现在只需要在antd中把功能实现就行了

实现步骤

  1. 为Table组件指定pagination属性来展示分页效果

            <TablerowKey="id"columns={columns}dataSource={list}pagination={{current: reqData.page,pageSize: reqData.per_page,total: count,onChange: onPageChange,}}/>
    

    添加了个pagination属性,是个对象,对象里面放总页数,以及当前页码,页码切换时的回调

  2. 在分页切换事件中获取到筛选表单中选中的数据

      //   分页实现const onPageChange = (page) => {setReqData({...reqData,page})}
    
  3. 使用当前页数据修改params参数依赖引起接口重新调用获取最新数据

    这一步在onPageChange方法中已经实现,因为改变了reqData,所以会重新获取数据

6. 表格数据删除

实现效果

image-20240329094107408

实现步骤

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

这里得仔细说一下,在vue中,删除表格中的数据,通常的做法是,首先使用框架比如elementPlus渲染表头,表格的最后一列设置固定的内容,就是删除(或者编辑)按钮,当点击按钮时,elementPlus会把这一行的数据传递出去,我只需要在删除方法中利用这条数据执行数据库删除就行了

看看vue中的写法

<el-table-column fixed="right" label="操作" width="250"><template #default="scope"><el-button type="primary" size="small" @click="handleApply(scope.$index, scope.row)":disabled="scope.row.product_out_status == '申请出库'">申请出库</el-button><el-button type="success" size="small" @click="handleEdit(scope.$index, scope.row)":disabled="scope.row.product_out_status == '申请出库'">修改</el-button><el-popconfirm title="确定删除该产品吗?" @confirm="confirmDelete(scope.row)"><template #reference><el-button type="danger" size="small" :disabled="scope.row.product_out_status == '申请出库'">删除</el-button></template></el-popconfirm></template>
</el-table-column>

在vue中,执行删除当前行的数据,不需要表头提供,scope提供了当前行的数据,在confirmDelete中执行相关的逻辑就行了

但是在react+antD中,逻辑变化很大

  • 删除的页面渲染放在了表头中
  • 传递当前数据用的是render方法
  • render传递默认参数就是当前行数据

先看看表头中是怎么渲染的

      render: (data) => {return (<Space size="middle"><Buttontype="primary"shape="circle"icon={<EditOutlined />}onClick={() => navigate(`/publish?id=${data.id}`)}/><Popconfirm title="删除?" onConfirm={() => onDelete(data)}><Buttontype="primary"dangershape="circle"icon={<DeleteOutlined />}/></Popconfirm></Space>);},

其实没啥好说的,思路差不多,但是写法实在是不能接受,相当混乱,在定义的数据中渲染html结构,以及包含处理逻辑,我凌乱了。。。

回到正文

其实现在就是要完成onDelete方法中的逻辑就行了

  1. 删除文章的接口

    export const deleteArticleListAPI = (data) => {return http({url: `/mp/articles/${data.id}`,method: "DELETE",});
    };
    

    接口需要传递文章id

  2. 页面调用接口并执行删除逻辑

      //   删除const onDelete = async row => {await deleteArticleListAPI({ id: row.id });getList()
    }
    

7. 编辑文章跳转publish页面

点击编辑文章时,应该跳转到编辑文章页面,并携带当前行数据过去

先实现跳转

	{title: "操作",render: (data) => {return (<Space size="middle"><Buttontype="primary"shape="circle"icon={<EditOutlined />}onClick={() => navigate(`/publish?id=${data.id}`)}/><Popconfirm title="删除?" onConfirm={() => onDelete(data)}><Buttontype="primary"dangershape="circle"icon={<DeleteOutlined />}/></Popconfirm></Space>);},},

六、编辑文章

1. 数据回填

从管理文章页面跳转过来的,需要把文章信息回填Form表单

这里就需要根据id获取数据了,需要用到router中的useSearchParams方法

实现步骤

  1. 编写根据id获取文章的接口

    export const getArticleAPI = (data) => {return http({url: `/mp/articles/${data.id}`,});
    };
    
  2. 根据路由参数获取id,并从后端获取文章数据

  3. 数据回填

      // 回填数据const [searchParams] = useSearchParams();const articleId = searchParams.get("id");const [form] = Form.useForm();const getArticle = async () => {// console.log(articleId)const res = await getArticleAPI({ id: articleId });const data = res.data;const { cover } = data;// console.log(res);form.setFieldsValue({...data,type: cover.type,});// 回填封面setImageType(cover.type);setImageList(cover.images.map((item) => {return { url: item };}));};
    
  4. 副作用函数中调用,并监控form数据的变化

      useEffect(() => {fetchChannels();if (articleId) getArticle();}, [articleId, form]);
    

2. 面包屑适配标题

根据是否有articleId适配

          <Breadcrumbitems={[{ title: <Link to={"/"}>首页</Link> },{ title: `${articleId ? '编辑文章' : '发布文章'}` },]}/>}

3. 更新文章

需要修改之前发布文章的代码,根据articleId调用不同的接口

实现步骤

  1. 编写编辑文章的接口

    // 编辑
    export const editAPI = (data) => {return http({url: `/mp/articles/${data.id}?draft=false`,method: "PUT",data,});
    };
    
  2. 修改onFinish方法,根据articleId调用不同的数据接口

      const onFinish = async (values) => {if (imageList.length !== imageType)return message.warning("封面图片和数量不一致");const { channel_id, content, title } = values;const data = {channel_id,content,title,cover: {type: imageType,images: imageList.map((item) => {if (item.response) {// 新增逻辑return item.response.data.url;} else {// 回填逻辑return item.url;}}),},};if (articleId) {// 编辑接口await editAPI({ ...data, id: articleId });message.success('编辑完成')} else {// 新增接口await publishAPI(data);message.success('发布成功')}navigate('/article')};
    

至此,完成了这个react项目的所有实现逻辑

而我,大概会继续选用vue3作为我的前端框架写下去~~~

最后上一下演示视频

视频地址

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【从前端入门到全栈】前端框架之核心概念

大家好&#xff0c;我是江辰&#xff0c;从前端入门到全栈是我全新系列文章&#xff0c;从去年一直囔囔着要写&#xff0c;今年总算开始了&#xff01;预计在10篇左右。知识面从 前端&#xff0c;后端&#xff0c;运维&#xff0c;脚本等&#xff0c;都有涉及&#xff0c;主打一…

C语言预处理详解

前言 上篇博客我们总结了编译与链接&#xff0c;有说过编译里第一步是预处理&#xff0c;那本篇博客将对预处理进行进一步的详细的总结 个人主页&#xff1a;小张同学zkf 若有问题 评论区见 感兴趣就关注一下吧 目录 1. 预定义符号 2. #define 定义常量 3. #define定义宏 4…

实践笔记-harbor搭建(版本:2.9.0)

harbor搭建 1.下载安装包&#xff08;版本&#xff1a;2.9.0&#xff09;2.修改配置文件3.安装4.访问harbor5.可能用得上的命令: 环境&#xff1a;centos7 1.下载安装包&#xff08;版本&#xff1a;2.9.0&#xff09; 网盘资源&#xff1a;https://pan.baidu.com/s/1fcoJIa4x…

睿尔曼超轻量仿人机械臂之复合机器人底盘介绍及接口调用

机器人移动平台是一个包含完整成熟的感知、认知和定位导航能力的轮式机器人底盘产品级平台&#xff0c;产品致力于为各行业细分市场的商用轮式服务机器人提供一站式移动机器人解决方案&#xff0c;让合作伙伴专注在核心业务/人机交互的实现。以下是我司产品双臂机器人以及复合升…

安装部署MariaDB数据库管理系统

目录 一、初始化MariaDB服务 1、安装、启动数据库服务程序、将服务加入开机启动项中。 2、为保证数据库安全性和正常运转&#xff0c;需要对数据库程序进行初始化操作。 3、配置防火墙&#xff0c;放行对数据库服务程序的访问请求&#xff0c;允许管理员root能远程访问数据…

使用 Spring Email 和 Thymeleaf 技术,向新注册用户发送激活邮件(一)

这篇内容对应"2.1 发送邮件"小节 邮箱设置 需要去邮箱对应的官方客户端软件或网站开启IMAP/SMTP服务或POP3/SMTP服务器 如果不开启&#xff0c;就无法使用第三方用户代理&#xff0c;只能走第官方的电子邮件客户端软件或网站&#xff0c;用户代理就是电子邮件客户…

2024-03-26 Android8.1 px30 WI-FI 模块rtl8821cu调试记录

一、kernel 驱动&#xff0c;我这里使用v5.8.1.2_35530.20191025_COEX20191014-4141这个版本&#xff0c;下载这个版本的驱动可以参考下面的文章。 2021-04-12 RK3288 Android7.1 USB wifi bluetooth 模块RTL8821CU 调试记录_rk平台rtl8821cu蓝牙调试-CSDN博客 二、Makefile文…

C++从入门到精通——引用()

C的引用 前言一、C引用概念二、引用特性交换指针引用 三、常引用保证值不变权限的方法权限的放大权限的缩小权限的平移类型转换临时变量 四、引用的使用场景1. 做参数2. 做返回值 五、传值、传引用效率比较值和引用的作为返回值类型的性能比较 六、引用和指针的区别引用和指针的…

web 技术中前端和后端交互过程

1、客户端服务器交互过程 客户端:上网过程中,负责浏览资源的电脑,叫客户端服务器:在因特网中,负责存放和对外提供资源的电脑叫服务器 服务器的本质: 就是一台电脑,只不过相比个人电脑它的性能高很多,个人电脑中可以通过安装浏览器的形式,访问服务器对外提供的各种资源。 个人…

Electron 读取本地配置 增加缩放功能(ctrl+scroll)

最近&#xff0c;一个之前做的electron桌面应用&#xff0c;需要增加两个功能&#xff1b;第一是读取本地的配置文件&#xff0c;然后记载配置文件中的ip地址&#xff1b;第二就是增加缩放功能&#xff1b; 第一&#xff0c;配置本地文件 首先需要在vue工程根目录中&#xff0…

切换ip地址的app,简单易用,保护隐私

在数字化时代&#xff0c;IP地址作为网络设备的标识&#xff0c;不仅承载着数据在网络间的传输任务&#xff0c;还在一定程度上关联着用户的隐私和安全。因此&#xff0c;切换IP地址的App应运而生&#xff0c;为用户提供了一种便捷的方式来改变其网络身份&#xff0c;实现匿名浏…

【Spring MVC】快速学习使用Spring MVC的注解及三层架构

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【Spring MVC】快速学习使用Spring MVC的注解及三层架构 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 Spring Web MVC一: 什么是Spring Web MVC&#xff1…

【应用笔记】LAT1413+快速开关蓝牙导致设备无广播

1. 问题背景 客户使用 BlueNRG-345MC 开发了一个 BLE 外设&#xff0c;和手机连接。在测试中发现&#xff0c;手机连接上外设之后&#xff0c;不断地在手机上点击蓝牙的开关按钮&#xff0c;造成设备不断地断开、重连&#xff1b;少则几次&#xff0c;多则几十次。点击之后&am…

【Entity Framework】创建并配置模型

【Entity Framework】创建并配置模型 文章目录 【Entity Framework】创建并配置模型一、概述二、使用fluent API配置模型三、分组配置四、对实体类型使用EntityTypeConfigurationAttribute四、使用数据注释来配置模型五、实体类型5.1 在模型中包含类型5.2 从模型中排除类型5.3 …

loadbalancer 引入与使用

在消费中pom中引入 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> 请求调用加 LoadBalanced 注解 进行服务调用 默认负载均衡是轮训模式 想要切换…

【数据结构与算法】二叉树的遍历及还原

树形结构 - 有向无环图 树是图的一种。 树形结构有一个根节点树形结构没有回路根节点&#xff1a;A叶子节点&#xff1a;下边没有其他节点了节点:既不是根节点,又不是叶子节点的普通节点树的度:这棵树最多叉的节点有多少叉&#xff0c;这棵树的度就为多少树的深度&#xff1a…

实例、构造函数、原型、原型对象、prototype、__proto__、原型链……

学习原型链和原型对象&#xff0c;不需要说太多话&#xff0c;只需要给你看看几张图&#xff0c;你自然就懂了。 prototype 表示原型对象__proto__ 表示原型 实例、构造函数和原型对象 以 error 举例 图中的 error 表示 axios 抛出的一个错误对象&#xff08;实例&#xff0…

WiFiSpoof for Mac wifi地址修改工具

WiFiSpoof for Mac&#xff0c;一款专为Mac用户打造的网络隐私守护神器&#xff0c;让您在畅游互联网的同时&#xff0c;轻松保护个人信息安全。 软件下载&#xff1a;WiFiSpoof for Mac下载 在这个信息爆炸的时代&#xff0c;网络安全问题日益凸显。WiFiSpoof通过伪装MAC地址&…

C++入门知识详细讲解

C入门知识详细讲解 1. C简介1.1 什么是C1.2 C的发展史1.3. C的重要性1.3.1 语言的使用广泛度1.3.2 在工作领域 2. C基本语法知识2.1. C关键字(C98)2.2. 命名空间2.2 命名空间使用2.2 命名空间使用 2.3. C输入&输出2.4. 缺省参数2.4.1 缺省参数概念2.4.2 缺省参数分类 2.5. …

GRE和MGRE综合实验

实际网段划分 分配IP 1.IP划分 [r1]int g0/0/0 [r1-GigabitEthernet0/0/0]ip add 192.168.1.254 24 Mar 29 2024 16:42:44-08:00 r1 %%01IFNET/4/LINK_STATE(l)[3]:The line protocol IP on the interface GigabitEthernet0/0/0 has entered the UP state. [r1-Gigabi…