React----中后台管理项目开发

丁俊爽
2023-12-01

中后台管理项目开发

使用react+antd实现中后台管理项目一些简单的项目功能

1.项目准备

1.1创建项目

//创建项目
pnpm create vite
//安装依赖
pnpm i
//启动项目
pnpm run dev

1.2.配置路由别名和服务器代理

在vite.config.js中配置

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    //设置根目录别名
    alias: {
      "@": path.resolve(__dirname, 'src')
    }
  },
  server: {
    //启动端口
    port: 8000,
    //服务器代理
    proxy: {
      "/api": {
        //请求地址
        target: "http://localhost:3000",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      }
    }
  }
})

1.3.安装插件

1.3.1安装技术栈

//sass 插件
pnpm i sass
//路由模块
npm i react-router-dom
//初始化样式
pnpm i normalize.css
//axios
pnpm i axios
//antd组件库和图标库
pnpm i antd @ant-design/icons
//redux状态管理工具
pnpm i react-redux @react/tookit

//全部安装
pnpm i sass react-router-dom normalize.css axios antd @ant-design/icons react-redux @react/tookit

1.3.2使用技术栈

(1)引入初始化样式

main.jsx

import "normalize.css"

(2)引入antd样式

antd组件库是按需加载,只需要先把css样式引入到根组件中,在组件中使用时按需引入

App.js

import 'antd/dist/reset.css';

2.项目开始

2.1配置根目录别名和服务器代理

在vite.config.js中添加配置项

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    //设置别名
    alias: {
      "@": path.resolve(__dirname, 'src')
    }
  },
  server: {
    port: 8000,
    //服务器代理
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      }
    }
  }
})

2.2配置路由

2.2.1在App.jsx中引入路由模块

import React from 'react'
// 引入antd样式
import 'antd/dist/reset.css';
import './App.scss'
// 引入路由模块
import { BrowserRouter } from 'react-router-dom';

// 国际化配置
import { ConfigProvider } from 'antd'
import zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');

// 引入路由列表
import Router from './router'

export default function App() {
  return (
     //国际化配置
    <ConfigProvider locale={zhCN}>
      {/*路由模块*/}
      <BrowserRouter>
        <Router></Router>
      </BrowserRouter>
    </ConfigProvider>
  )
}

2.2.2根目录创建router文件夹存放路由文件

先创建好基础的目录,layout,login,404/Notfound

创建index.jsx文件使用useRoutes( )生成路由

router/index.jsx

import React from 'react'
// 引入路由模块
import { useRoutes } from 'react-router-dom'
// 引入路由列表
import RouterList from './RouterList'

export default function index() {
    // console.log(RouterList);
    const router = useRoutes(RouterList)
    return router
}

创建RouterList.jsx文件,使用路由懒加载生成路由列表导入到router/index.jsx

这里用auth属性来判断进入页面是否需要token验证,一种是给不需要验证的页面加auth,一种给需要验证的页面加auth。

然后如果出现了进入页面的一瞬间布局混乱,可能是因为auth属性的问题,确认一下要跳转的路由是否需要有auth属性

router/RouterList.jsx

//使用懒加载 
import { lazy } from 'react'
// 路由跳转
import { Navigate } from 'react-router-dom'
// 处理路由懒加载的白屏问题
import LazyRouter from './LazyRouter'
// 引入组件路径(登录和NotFound一般不需要懒加载)
const Layout = lazy(() => import('@/pages/layout'))
const Login = lazy(() => import('@/pages/login'))
const NotFound = lazy(() => import('@/pages/404'))
const Index1 = lazy(() => import('@/pages/dashboard/index1'))
const Index2 = lazy(() => import('@/pages/dashboard/index2'))

const routerList = [{
    // 主页
    path: '/',
    //auth:进入路由是否需要token验证
    //title:该页的页面标题
    element: <Navigate to={'/dashboard/index'}></Navigate>
}, {
    // 首页
    path: '/dashboard',
    element: <LazyRouter element={<Layout />} title="主页面"></LazyRouter>,
    children: [{
        //主页面必须要嵌套路由,第一个空路由可以自动跳转到某个路由
        path: "",
        element: <Navigate to={'index'} />
    }, {
        path: 'index1',
        element: <LazyRouter element={<Index1 />} title="页面1" ></LazyRouter>
    }, {
        path: 'index2',
        element: <LazyRouter element={<Index2 />} title="页面2"></LazyRouter>
    }]
}, {
    // 登录页
    path: '/login',
    element: <LazyRouter element={<Login />} title="登录"></LazyRouter>
}, {
    // 空页
    path: '/404',
    element: <LazyRouter element={<NotFound />} title="找不到网页"></LazyRouter>
}, {
    // 找不到
    path: '*',
    element: <Navigate to={'/404'}></Navigate>
},]
export default routerList

创建LazyRouter.jsx文件,使用Suspense组件解决路由懒加载网速慢时的白屏问题

router/LazyRouter.jsx

import { Suspense } from 'react'
import { Spin } from 'antd';

// 路由拦截登录验证
import AuthRouter from './AuthRouter'
//渲染页面
import RenderRouter from './RenderRouter'

export default function LazyRouter({ auth, ...props }) {
    return (
        <Suspense
            fallback={
                <RenderRouter element={<Spin tip="Loading"><div /></Spin>}/>
            }>
            {/* 需要验证前往token验证,不需要验证就去渲染页面 */}
            {auth ? <RenderRouter {...props}></RenderRouter> : <AuthRouter  {...props}></AuthRouter>}
        </Suspense>
    )
}

创建AuthRouter.jsx文件进行token验证

router/AuthRouter.js

import React from 'react'
import { Navigate } from 'react-router-dom'
// 路由页面渲染
import RenderRouter from './RenderRouter'

// token验证
export default function AuthRouter({ ...props }) {
    const istoken = sessionStorage.getItem('token')
    return (
        istoken ? <RenderRouter {...props}></RenderRouter> : <Navigate to={'/login'}></Navigate>
    )

创建RenderRouter.jsx文件,进行路由页面渲染以及对页面一些配置

router/RenderRouter.jsx

import React from 'react'
// 渲染路由
export default function RenderRouter({ title, element }) {
    //配置页面的标题
    title ? document.title = title : null
    return element
}

2.3 api封装

2.3.1根目录创建api文件夹

创建config.js文件,配置网络请求信息

api/config.js

import axios from "axios" //第一步:导入axios
import { message } from 'antd'
//第二步:创建axios实例
const service = axios.create({
    baseURL: '/api',
    timeout: 50000//超时时间
})

//写好的拦截器,也可以先不用拦截器,全部注释
//第三步 请求拦截
service.interceptors.request.use((config) => {
    // 在发送请求之前做些什么 验证token之类的
    // console.log("请求的数据:", config);
    if (sessionStorage.token) {
        // 不再加common
        config.headers.Authorization = 'Bearer ' + sessionStorage.token
    }
    return config;
}, (error) => {
    return Promise.reject(error)
}
)
// //第四步 响应拦截
service.interceptors.response.use((response) => {
    return response
},
    (error) => {
        const { code } = error;
        if (code == "ERR_NETWORK") {
            //@ts-ignore
            message.error("没网");
        } else if (code == 401) {
            //@ts-ignore
            message.error("token过期啦,请重新登陆");
            sessionStorage.clear()
            window.location.reload()
        } else {
            message.error(error?.response?.data?.msg);
        }
        //@ts-ignore
        // return Promise.reject(error);
    }
)
export default service;

2.3.2创建http.js文件,管理网络请求

api/http.js

import axios from './config'
//用户登录
// 第一种写法
// export const login = data => axios.post("/login", data)

// 写好的网络请求,按照需求写
// 第二种写法
export function login(data) {
    return axios({
        url: '/login',
        method: "post",
        data
    })
}

2.4 登录页面

antd有时加载特别缓慢

使用了antd的栅格进行界面定位

使用了antd的表单布置登录表单

添加了保存账号和密码的逻辑,在初始化时会到本地存储中寻找

服务器验证通过后保存token,登录成功跳转页面

login/index.jsx

import './Login.scss'
// 引入antd组件
import { Row, Col, Button, Radio, Form, Input, message, Checkbox } from 'antd';
import { login } from '@/api/http';
import { useNavigate } from 'react-router-dom'
import { useEffect } from 'react';
import { useForm } from 'antd/es/form/Form';

// 登录信息初始值
let init = {
    remember: true,
    account: '',
    pw: '',
    power: "0",
}
// 自定义表单校验
const rulespw = (rule, value) => {
    if (value.length === 0) Promise.reject("密码不能为空")
    if (!(/^[\da-z]{3,8}$/.test(value))) Promise.reject("密码应该是3-8位的数字或小写字母")
        return Promise.resolve()
}

export default function index() {
    const navigate = useNavigate()
    //使用Antd的自定义Hook来获取表单域
    const [loginForm] = useForm()
    // 判断是否存储的有账号和密码
    const rememberUser = () => {
        let user = JSON.parse(localStorage.getItem('user'))
        if (user) loginForm.setFieldsValue(user)
    }
    // 初始化
    useEffect(() => {
        rememberUser()
    },[])
    
    // 表单验证成功的事件
    const onFinish = async (values) => {
        // 向服务器验证
        let { code, token } = (await login(values)).data
        // console.log((await login(values)));
        if (code === 0) {
            // 服务器验证成功保存token并跳转
            sessionStorage.setItem('token', token)
            message.success('登录成功', 1)
            navigate("/")
            // 是否在本地保存账号密码
            values.remember?
                localStorage.setItem('user', JSON.stringify(values))
            	:localStorage.removeItem('user')
        } else {
            // 登录失败
            onFinishFailed()
        }
    };
    // 登录失败
    const onFinishFailed = (errorInfo) => message.error('登录失败', 1)
    
    return (
        <Row className='login' >
            <Col span={6} offset={9} className='wrapper'>
                <Form
                    form={loginForm}
                    //标签宽度
                    labelCol={{ span: 4 }}
                    wrapperCol={{ span: 20 }}
                    // 初始值
                    initialValues={init}
                    autoComplete="off"
                    // 提交
                    onFinish={onFinish}
                    onFinishFailed={onFinishFailed}
                >
                    <Form.Item
                        label="账号"
                        name="account"
                        // 简单表单校验
                        rules={[
                            { required: true, message: '请输入账号!' },
                            { pattern: /^\w{3,7}$/, message: '账号为3-7位的字符' }
                        ]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item label="密码" name="pw" rules={[{ validator: rulespw }]}>
                        <Input.Password />
                    </Form.Item>
                    <Form.Item label="身份" name="power">
                        <Radio.Group buttonStyle="solid">
                            <Radio.Button value="0">超级管理员</Radio.Button>
                            <Radio.Button value="1">管理员</Radio.Button>
                            <Radio.Button value="2">普通用户</Radio.Button>
                        </Radio.Group>
                    </Form.Item>
                    <Form.Item wrapperCol={{ offset: 4 }} name="remember" valuePropName="checked">
                        <Checkbox>记住密码</Checkbox>
                    </Form.Item>
                    <Form.Item wrapperCol={{ span: 24 }}>
                        <Button type="primary" htmlType="submit" size='large' className='loginbutton' >
                            登录
                        </Button>
                    </Form.Item>
                </Form>
            </Col>
        </Row >
    )
}

2.5主界面布局

在layout/index.jsx文件中进行主界面的布局

对侧边栏,头部,内容区进行了拆分

layout/index.jsx

import React, { useState } from 'react';
import './Layout.scss'
import { Outlet } from 'react-router-dom';
import { Layout, theme } from 'antd';
const { Header, Sider, Content } = Layout;
import Menu from './sider/SiderMenu';
import LayoutHeader from './header/LayoutHeader';
import TagsRouter from './TagsRouter';

export default function layout() {
    // 侧边栏收起
    const [collapsed, setCollapsed] = useState(false);
    // 主题
    const {
        token: { colorBgContainer },
    } = theme.useToken();
    
    return (
        <Layout className='admin_layout'>
            <Sider trigger={null} collapsible collapsed={collapsed}>
                <div className="logo" />
                {/* 导航菜单 */}
                <Menu></Menu>
            </Sider>
            <Layout>
                <Header style={{ background: colorBgContainer }}>
                    {/*组件化*/}
                    <LayoutHeader collapsed={collapsed} changeCollapsed={() => setCollapsed(!collapsed)}></LayoutHeader>
                </Header>
                {/* tag标签 */}
                <TagsRouter></TagsRouter>
                <Content className='layout-content'>
                    <Outlet />
                </Content>
            </Layout>
        </Layout>
    )
}

2.6导航菜单

2.6.1.获取菜单数据

创建创建sider文件夹存放侧边栏的组件,创建MenuList.js文件用来存放菜单数据

可能是要向后台请求的数据,但是数据格式不会有太大差别

如果有图标用字符串的方式,不能记录组件

layout/sider/MenuList.js

// 导航菜单列表
const MenuList = [{
    key: '/dashboard',
    icon: "UserOutlined",
    label: '一级菜单',
    children: [{
        key: '/dashboard/index1',
        icon: "UserOutlined",
        label: '页面1',
    }, {
        key: '/dashboard/index2',
        icon: "UserOutlined",
        label: '页面2',
    }]
}]
export default MenuList

2.6.2处理菜单中的图标组件

这个功能可以写成可复用的组件,在根目录创建compented文件夹

创建IconFont.js文件,使用React的createElement方法将图标转换成组件

compented/IconFont.js

import React from 'react'
import * as Icons from '@ant-design/icons';
const Iconfont = (props) => {
    const { icon } = props
    //React提供的createElement方法可以将字符串转成组件
    return React.createElement(Icons[icon])
}
export default Iconfont

2.6.3添加导航菜单

NavMenu.jsx文件用来配置导航菜单组件

layout/sider/NavMenu.jsx

import React, { useEffect, useState } from 'react'
import { Menu } from 'antd';
import { useNavigate, useLocation } from 'react-router-dom';
import MenuList from './MenuList';
import Iconfont from '@/compented/IconFont';

// 处理菜单的图标
const handleMenuData = (list) => {
    // 深拷贝防止污染原数据
    let items = JSON.parse(JSON.stringify(list))
    return items.map(item => {
        //调用写好的方法将图标转成字符串
        item.icon = item.icon && <Iconfont icon={item.icon}></Iconfont>
        //如果有子菜单,需要用到递归
        item.children = item.children && handleMenuData(item.children)
        return item
    })
}

export default function SiderMenu() {
    const navigate = useNavigate()
    const { pathname } = useLocation()
    // 处理初始化时高亮和展开的菜单项
    let selectedKeys = pathname
    let openKeys = '/' + pathname.split('/')[1]
    // 添加点击导航跳转
    const onSelect = ({ key }) => navigate(key)
    // 初始化导航菜单
    const [menuList, setMenuList] = useState([])
    useEffect(() => {
        setMenuList(handleMenuData(MenuList))
    }, [])

    return (
        <Menu
            theme="dark"
            mode="inline"
            // 默认高亮
            defaultSelectedKeys={[selectedKeys]}
            // 默认展开
            defaultOpenKeys={[openKeys]}
            // 选中事件
            onSelect={onSelect}
            // 菜单列表
            items={menuList}
        />
    )
}

2.7添加面包屑导航

创建header文件夹来存放头部的组件

创建LayoutHeader.jsx文件,拆分layout布局的header部分

/layout/header/LayoutHeader.jsx

import React from 'react'
import {
    MenuFoldOutlined,
    MenuUnfoldOutlined,
} from '@ant-design/icons';
import HeaderBreadcrumb from './HeaderBreadcrumb';
import { Button } from 'antd';
import { useNavigate } from 'react-router-dom';

export default function LayoutHeader(props) {
    
    const navigate = useNavigate()
    //退出登录清除token,转到主页
    const edit = () => {
        sessionStorage.removeItem('token')
        navigate('/')
    }
    let { collapsed, changeCollapsed } = props
    return (
        <div className="layout_header">
            {/* 是否收起侧边栏 */}
            {React.createElement(
                collapsed ? MenuUnfoldOutlined : MenuFoldOutlined,
                { className: 'trigger', onClick: () => changeCollapsed() }
            )}
            {/* 面包屑 */}
            <HeaderBreadcrumb></HeaderBreadcrumb>
            <Button type="primary" danger style={{}} onClick={edit}>
                退出登录
            </Button>
        </div>
    )
}

需要有一个路由和标题对应的数据

JS-数据处理:https://blog.csdn.net/jddfj/article/details/129208198

在 compented文件夹中创建MenuKeyAndLaber.js文件用来处理菜单数据

/compented/MenuKeyAndLaber.js

// 获取菜单中路由和标题的信息
const menuKeyAndLaber = (lists) => {
    const list = JSON.parse(JSON.stringify(lists))
    let newObj = {}
    const degui = (list) => {
        list.forEach(item => {
            for (const key in item) {
                if (key === "key") {
                    newObj[item.key] = item.label
                }
                if (key === 'children') {
                    //子菜单需要递归
                    degui(item[key])
                }
            }
        });
    }
    degui(list)
    return newObj
}
export default menuKeyAndLaber

创建一个HeaderBreadcrumb.jsx文件

import React from 'react'
import MenuList from '../sider/MenuList'
import { Breadcrumb } from 'antd';
import { useLocation } from 'react-router-dom';
import { useEffect,useState } from 'react';
import menuKeyAndLaber from "@/compented/MenuKeyAndLaber"

export default function Breadcrumbsd() {
    
    // 获取地址栏路径并拆分成数组
    const location = useLocation();
    const pathSnippets = location.pathname.split('/').filter((i) => i);
    // 存放路由和标题对应信息
    const [breadcrumbNameMap, setbreadcrumbNameMap] = useState([])
    //初始化时获取菜单的信息
    useEffect(() => {
        setbreadcrumbNameMap(menuKeyAndLaber(MenuList))
    }, [])
    // 根据路由生成面包屑
    const extraBreadcrumbItems = pathSnippets.map((_, index) => {
        /**
        *原本:['/one','/two']
        *遍历后:['/one','/one/two']
        */
        // 根据路由数组循环遍历
        const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
        // console.log(url);
        return (
            <Breadcrumb.Item key={url}>
                {/*根据路由到数据中获取标题*/}
                <span>{breadcrumbNameMap[url]}</span>
            </Breadcrumb.Item>
        );
    });
    // 面包屑导航主题
    const breadcrumbItems = [
        {/*默认有一个首页,也可以没有,看需求*/}
        <Breadcrumb.Item key="home">
            <span>首页</span>
        </Breadcrumb.Item>,
        ...extraBreadcrumbItems
    ]

    return (
        <div className="breadcrumb">
            <Breadcrumb>{breadcrumbItems}</Breadcrumb>
        </div>
    )
}

2.8添加tab标签

用到了redux和@reduxjs/toolkit

创建一个TabsRouter.jsx封装tabs组件

/layout/herder/TabsRouter.jsx

这里是完成之后的代码,使用了redux管理工具,刚开始写时可以用官网上的固定数据,然后在redux添加tabsList数据,完成之后将数据替换即可

import React, { useEffect } from 'react'
import { Tabs } from 'antd';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { changeActiveKey, editTabs } from '@/store/tabsStore';

export default function TagsRouter() {
    const navigate = useNavigate()
    const dispatch = useDispatch()
    // 从仓库获取tabs列表和高亮
    const { tabsList, activeKey } = useSelector(state => state.tabs)

    // 点击tabs标签进行路由跳转
    const onChange = (key) => {
        navigate(key)
        // console.log(key);
        // 修改仓库的高亮
        dispatch(changeActiveKey(key))
    };
	//根据依赖获取高亮
    useEffect(() => {
        navigate(activeKey)
    }, [activeKey])
    // 删除tabs标签
    const onEdit = (key, remove) => {
        if (remove === 'remove') {
            dispatch(editTabs(key))
        }
    };
    return (
        <Tabs 
            activeKey={activeKey} 
            items={tabsList}
            onChange={onChange}
            closable={
                tabsList.closable ?
                    tabsList.closable 
                	: ''
            }
            onEdit={onEdit}
            type="editable-card" 
            hideAdd 
        />
    )
}

redux的使用流程以这个项目的tabs标签为例做了单独的总结,这里只总结了逻辑代码

React----react-redux和@react/tookit的使用流程:https://blog.csdn.net/jddfj/article/details/129361881

在tabsStore.js

import { createSlice } from "@reduxjs/toolkit";
//数据持久化
const initialState = JSON.parse(localStorage.getItem('initialState'))
const tabsStore = createSlice({
    name: "tabs",
    initialState: initialState ? initialState :
        {
            // tabs标签列表
            tabsList: [{
                key: '/dashboard/console',
                label: '主控台',
                closable: false
            }],
            // 高亮标签
            activeKey: '/dashboard/console'
        },
    reducers: {
        // 添加tabs标签
        changeTabs: (state, { payload }) => {
            let flag = state.tabsList.some(item => item.key === payload.key)
            state.activeKey = payload.key
            // tabsList中是否已经存在
            flag ? null : state.tabsList.push(payload)
            //数据持久化
            localStorage.setItem('initialState', JSON.stringify(state))
        },
        // 改变高亮
        changeActiveKey: (state, { payload }) => {
            state.activeKey = payload
            //数据持久化
            localStorage.setItem('initialState', JSON.stringify(state))
        },
        // 删除tabs标签
        editTabs: (state, { payload }) => {
            let i = state.tabsList.findIndex(item => item.key === payload)
            state.tabsList.splice(i, 1)
            // 删除最后一项
            if (i === state.tabsList.length && payload === state.activeKey) {
                state.activeKey = state.tabsList[i - 1].key
            }
            // 删除自己
            if (payload === state.activeKey) {
                state.activeKey = state.tabsList[i].key
            }
            // 删除最后一个
            if (i === 0 && state.tabsList.length === 1) {
                state.activeKey = ''
            }
            //数据持久化
            localStorage.setItem('initialState', JSON.stringify(state))
        }
    }
})
export const { changeTabs, changeActiveKey, editTabs } = tabsStore.actions
export default tabsStore.reducer

2.9 信息管理

2.9.1 添加路由

创建对应的文件,并在路由列表内添加信息管理的路由,添加班级管理的二级路由

/router/RouterList.jsx

...
const StudentInfo = lazy(() => import('@/pages/student/StudentInfo'))
const ClassInfo = lazy(() => import('@/pages/student/ClassInfo'))
const EditStudent = lazy(() => import('@/pages/student/studentInfo/EditStudent'))
...

const routerList = [{
    ...
     {
        // 信息管理
        path: '/student',
        element: <LazyRouter element={<Layout />} title="信息管理"></LazyRouter>,
        children: [{
            path: "",
            element: <Navigate to={'studentinfo'} />
        }, {
            path: 'studentinfo',
            element: <LazyRouter element={<StudentInfo />} title="学生管理" ></LazyRouter>
        }, {
            path: 'classinfo',
            element: <LazyRouter element={<ClassInfo />} title="班级管理"></LazyRouter>
        }, {
            path: 'editstudent',
            element: <LazyRouter element={<EditStudent />} title="编辑学生" ></LazyRouter>
        },]
	},
    ...
}]
 
export default routerList

2.9.2 列表展示

首先完成最基础的班级列表的展示

/pages/student/ClassInfo.jsx

import React, { useState, useEffect } from 'react'
import { Card, Table, Button, Space, Tag, } from 'antd';
import { getClassList } from '@/api/http';
import day from 'dayjs'

export default function ClassInfo() {
    // 表格字段
    const ClasstableColumns = [
        {
            title: '班级',
            dataIndex: 'className',
            key: 'className',
        }, {
            title: '班主任',
            dataIndex: 'ht',
            key: 'ht',
        }, {
            title: '开班时间',
            dataIndex: 'startDate',
            key: 'startDate',
            render: (text) => day(text).format('YYYY-MM-DD')
        }, {
            title: '开班状态',
            dataIndex: 'classStatus',
            key: 'classStatus',
            render: (classStatus) => {
                return (
                    <Tag color={classStatus === 0 ? 'default' : (classStatus === 1 ? 'warning' : 'success')}>
                        {classStatus === 0 ? '即将开班' : (classStatus === 1 ? '已结课' : '已开班')}
                    </Tag>
                )
            }
        }, {
            title: '操作',
            key: 'action',
            render: (_, record) => (
                <Space size="middle">
                    <Button type="primary">编辑</Button>
                    <Button danger>删除</Button>
                </Space>
            ),
        },
    ];
    // 表格数据
    const [tabledata, setTableData] = useState([])
    // 页码
    const [total, setTotal] = useState(0)
    const [page, setPage] = useState(1)
    // 请求班级数据
    const getClassData = async () => {
        //从后端获得了班级列表和表格总条数
        let { code, list, total } = (await getClassList({ page })).data
        if (code === 0) {
            setTableData(list)
            setTotal(total)
        } else {
            setTableData([])
            setTotal(0)
        }
    }
    // 渲染表格
    useEffect(() => {getClassData()}, [page])
    // 改变页码
    const onChange = (page) =>  setPage(page)
    
    return (
        <Card title="班级信息管理" extra={<Button>添加班级</Button>}>
            <Table
                style={{ minWidth: '650px' }}
                columns={ClasstableColumns}
                dataSource={tabledata}
                rowKey={(record) => String(record.classId)}
                // 分页
                pagination={{
                    total,
                    pageSize: 4,
                    current: page,
                    onChange
                }}
            />
        </Card>
    )
}

2.9.3 添加记录

创建一个classInfo文件夹,用来存储student相关的组件

这里是用的modal框实现的添加编辑,还可以用路由跳转的方式

创建EditClass.jsx文件,将添加和编辑写成同一个组件

下面是只写了添加逻辑的代码

/student/classInfo/EditClass.jsx

import {
    Button,
    Modal,
    Spin,
    Form,
    Input,
    Radio,
    DatePicker,
    Select,
    message
} from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { useState } from 'react';
import { useForm } from 'antd/es/form/Form';
import { _addClass } from '@/api/http';
import day from 'dayjs'
//这个数据应该是由后台获取的,这里写成了固定数据
const htList = ['高启强', '高启盛', '高启兰', '安欣', '孟钰', '史强', '汪淼']

export default function EditClass(props) {
    // 接收数据
    let { open, setOpen, setnewClass, pageOne } = props
    // 表单实例
    const [editClassForm] = useForm()
    // 等待动画
    const [loading, setloading] = useState(false);
    // 对话框确认
    const handleOk = async () => {
        // 提交表单 触发表单onFinish事件
        editClassForm.submit()
    };
    // 对话框取消
    const handleCancel = () => {
        // 关闭并清空
        setOpen(false)
        editClassForm.resetFields()
    }
    // 表单提交
    const onFinish = async (values) => {
        setloading(true);
        let req = ''
        // 处理参数的时间日期格式
        values.startDate = day(values.startDate).format("YYYY-MM-DD")
        req = (await _addClass(values)).data
        let { code, msg } = req
        // 添加成功后关闭卡片并清空表单
        if (code === 0) {
            message.success(msg)
            handleCancel()
            // 触发表格刷新
            setnewClass()
            pageOne(1)
        } else {
            message.error(msg)
        }
        setloading(false);
    }

    return (
        <Modal
            title="添加班级"
            open={open}
            onCancel={handleCancel}
            footer={
                <div>
                    <Button type='primary' onClick={() => handleOk()} disabled={loading}>添加"</Button>
                    <Button onClick={handleCancel}>取消</Button>
                </div>
            }
        >
            <Spin indicator={<LoadingOutlined />} spinning={loading}>
                <Form
                    labelCol={{ span: 6 }}
                    wrapperCol={{ span: 8 }}
                    style={{ minWidth: 400 }}
                    form={editClassForm}
                    onFinish={onFinish}
                    autoComplete="off"
                >
                    <Form.Item
                        label="班级"
                        name='className'
                        rules={[{ required: true, message: '班级不能为空' }]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item
                        label="班主任"
                        wrapperCol={{ span: 16 }}
                        name='ht'
                        rules={[{ required: true, message: '请选择班主任' }]}
                    >
                        <Radio.Group>
                            {htList.map((item) => <Radio value={item} key={item}>{item}</Radio>)}
                        </Radio.Group>
                    </Form.Item>
                    <Form.Item
                        label="开班时间"
                        name='startDate'
                        rules={[{ required: true, message: '请选择开班时间' }]}
                    >
                        <DatePicker />
                    </Form.Item>
                    <Form.Item
                        label="开班状态"
                        wrapperCol={{ span: 16 }}
                        name='classStatus'
                        rules={[{ required: true, message: '请选择开班状态' }]}
                    >
                        <Radio.Group value={0}>
                            <Radio value={0}>即将开班</Radio>
                            <Radio value={1}>已结课</Radio>
                            <Radio value={2}>已开班</Radio>
                        </Radio.Group>
                    </Form.Item>
                </Form>
            </Spin>
        </Modal>
    )
}

2.9.4 编辑记录

编辑记录跟添加记录使用的同一个组件,唯一的区别是,编辑记录是传递了初始值,然后渲染到页面上,

下面是添加了编辑逻辑的代码

/student/classInfo/EditClass.jsx

import {
    Button,
    Modal,
    Spin,
    Form,
    Input,
    Radio,
    DatePicker,
    Select,
    message
} from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { useState } from 'react';
import { useForm } from 'antd/es/form/Form';
import { _addClass, editClass } from '@/api/http';
import day from 'dayjs'
const htList = ['高启强', '高启盛', '高启兰', '安欣', '孟钰', '史强', '汪淼']

export default function EditClass(props) {
    // 接收数据
    let { open, setOpen, setnewClass, pageOne, editClassInfo, edit } = props
    // 表单数据处理
    const [editClassForm] = useForm()
    // 等待动画
    const [loading, setloading] = useState(false);
    // 对话框确认
    const handleOk = async () => {
        // 提交表单
        editClassForm.submit()
    };
    // 对话框取消
    const handleCancel = () => {
        // 关闭并清空
        setOpen(false)
        editClassForm.resetFields()
    }
    // 表单提交
    const onFinish = async (values) => {
        setloading(true);
        let req = ''
        // 处理时间格式
        values.startDate = day(values.startDate).format("YYYY-MM-DD")
        if (edit) {
            // 编辑
            // 将classId添加到参数中
            values.classId = editClassInfo.classId
            req = (await editClass(values)).data
        } else {
            //添加
            req = (await _addClass(values)).data
        }
        let { code, msg } = req
        // 添加成功后关闭卡片并清空表单
        if (code === 0) {
            message.success(msg)
            handleCancel()
            // 触发表格刷新
            setnewClass()
            pageOne(1)
        } else {
            message.error(msg)
        }
        setloading(false);
    }

    return (
        <Modal
            title={editClassInfo ? "编辑班级" : "添加班级"}
            open={open}
            onCancel={handleCancel}
            footer={
                <div>
                    <Button type='primary' onClick={() => handleOk()} disabled={loading}>{editClassInfo ? "保存" : "添加"}</Button>
                    <Button onClick={handleCancel}>取消</Button>
                </div>
            }
        >
            <Spin indicator={<LoadingOutlined />} spinning={loading}>
                <Form
                    labelCol={{ span: 6 }}
                    wrapperCol={{ span: 8 }}
                    style={{ minWidth: 400 }}
                    initialValues={{
                        ...editClassInfo,
                        {/*这里是对时间进行了一个格式的处理*/}
                        startDate: editClassInfo.startDate ? day(editClassInfo.startDate) : null
                    }}
                    form={editClassForm}
                    onFinish={onFinish}
                    autoComplete="off"
                >
                    <Form.Item
                        label="班级"
                        name='className'
                        rules={[{ required: true, message: '班级不能为空' }]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item
                        label="班主任"
                        wrapperCol={{ span: 16 }}
                        name='ht'
                        rules={[{ required: true, message: '请选择班主任' }]}
                    >
                        <Radio.Group>
                            {htList.map((item) => <Radio value={item} key={item}>{item}</Radio>)}
                        </Radio.Group>
                    </Form.Item>
                    <Form.Item
                        label="开班时间"
                        name='startDate'
                        rules={[{ required: true, message: '请选择开班时间' }]}
                    >
                        <DatePicker />
                    </Form.Item>
                    <Form.Item
                        label="开班状态"
                        wrapperCol={{ span: 16 }}
                        name='classStatus'
                        rules={[{ required: true, message: '请选择开班状态' }]}
                    >
                        <Radio.Group value={0}>
                            <Radio value={0}>即将开班</Radio>
                            <Radio value={1}>已结课</Radio>
                            <Radio value={2}>已开班</Radio>
                        </Radio.Group>
                    </Form.Item>
                </Form>
            </Spin>
        </Modal>
    )
}

2.9.5 查询记录

查询记录也是采用了组件的方式,这里是后台直接会根据传递的参数返回对应的结果,所以涉及的主要是组件传参

在classInfo文件夹下创建一个SelectClass.jsx组件

/student/classInfo/SelectClass.jsx

import {
    Button,
    Form,
    Input,
    Radio,
    DatePicker,
    Select,
    Space,Col,Row
} from 'antd';
import { useForm } from 'antd/es/form/Form';
import { _addClass } from '@/api/http';
import day from 'dayjs'
const htList = ['高启强', '高启盛', '高启兰', '安欣', '孟钰', '史强', '汪淼']

export default function SearchClass(props) {
    // 接收数据(改变搜索参数的方法,改变page的方法,触发数据更新的方法)
    let { setSearch, setPage, setNewClass } = props
    // 搜索表单实例
    const [SearchClassForm] = useForm()
    // 表单提交
    const onFinish = async (values) => {
        //改变时间日期格式
        values.startDate = values.startDate ? day(values.startDate).format("YYYY-MM-DD") : null
        //改变查询参数
        setSearch(values)
        //触发数据更新
        setNewClass()
    }
    // 表单重置
    const onReset = () => {
        SearchClassForm.resetFields()
        setPage(1)
        setSearch({})
        setNewClass()
    }
    return (
        <Form
            wrapperCol={{ offset: 1 }}
            style={{ minWidth: 400 }}
            form={SearchClassForm}
            onFinish={onFinish}
            autoComplete="off"
        >
            <Row gutter={[50, 0]} wrap>
                <Col>
                    <Form.Item
                        label="班主任"
                        wrapperCol={{ span: 24 }}
                        name='ht'
                    >
                        <Radio.Group>
                            {htList.map((item) => <Radio value={item} key={item}>{item}</Radio>)}
                        </Radio.Group>
                    </Form.Item>
                </Col>
                <Col>
                    <Form.Item
                        label="开班时间"
                        name='startDate'
                    >
                        <DatePicker />
                    </Form.Item>
                </Col>
                <Col>
                    <Form.Item
                        label="开班状态"
                        wrapperCol={{ span: 24 }}
                        name='classStatus'
                    >
                        <Radio.Group value={0}>
                            <Radio value={0}>即将开班</Radio>
                            <Radio value={1}>已结课</Radio>
                            <Radio value={2}>已开班</Radio>
                        </Radio.Group>
                    </Form.Item>
                </Col>
            </Row>
            <Row justify='end'>
                <Col>
                    <Form.Item wrapperCol={{ span: 24 }}>
                        <Space>
                            <Button type='primary' htmlType="submit">查询</Button>
                            <Button htmlType="button" onClick={onReset}>重置</Button>
                        </Space>
                    </Form.Item>
                </Col>
            </Row>
        </Form>
    )
}

2.9.6 删除记录

删除记录比较简单,只需要一个Id值,所以在列表展示的组件中直接添加逻辑。主要注意的是删除时处理边界问题(删除最后一条时;只剩最后一条时…)

下面是完整的列表展示的页面代码(包括了添加,编辑,删除,搜索的逻辑)

/pages/student/ClassInfo.jsx

import React, { useState, useEffect } from 'react'
import { Card, Table, Button, Space, Tag,message } from 'antd';
import { getClassList, delClass } from '@/api/http';
import day from 'dayjs'
// 添加班级
import EditClass from './classInfo/EditClass';
// 查询班级
import SearchClass from './classInfo/SearchClass';

export default function ClassInfo() {
    // 表格字段
    const ClasstableColumns = [{
            title: '班级',
            dataIndex: 'className',
            key: 'className',
        }, {
            title: '班主任',
            dataIndex: 'ht',
            key: 'ht',
        }, {
            title: '开班时间',
            dataIndex: 'startDate',
            key: 'startDate',
            //处理时间格式
            render: (text) => day(text).format('YYYY-MM-DD')
        }, {
            title: '开班状态',
            dataIndex: 'classStatus',
            key: 'classStatus',
            render: (classStatus) => {
                return (
                    <Tag color={classStatus === 0 ? 'default' : (classStatus === 1 ? 'warning' : 'success')}>
                        {classStatus === 0 ? '即将开班' : (classStatus === 1 ? '已结课' : '已开班')}
                    </Tag>
                )
            }
        }, {
            title: '操作',
            key: 'action',
            render: (_, record) => (
                <Space size="middle" key={3}>
                    <Button type="primary" onClick={() => editClass(record)}>编辑</Button>
                    <Button danger onClick={() => onDelete(record.classId)}>删除</Button>
                </Space>
            ),
        },
    ];

    // 表格数据
    const [tabledata, setTableData] = useState([])
    // 分页器
    const [total, setTotal] = useState(0)
    const [page, setPage] = useState(1)
    // 添加班级
    const [openAddStudent, setOpenAddStudent] = useState(false)
    // 设定一个标识用来触发数据更新
    const [newClass, setNewClass] = useState(true)
    // 编辑班级
    const [editClassInfo, setEditClassInfo] = useState({})
    const [edit, setEdit] = useState(false)
    // 查询班级
    const [search, setSearch] = useState({})
    // 请求班级数据
    const getClassData = async () => {
        let { code, list, total } = (await getClassList({
            page,
            //传递要查询的参数
            ...search
        })).data
        if (code === 0) {
            setTableData(list)
            setTotal(total)
        } else {
            setTableData([])
            setTotal(0)
        }
    }
    // 渲染表格,根据依赖触发数据更新
    useEffect(() => { getClassData() }, [page, newClass])
    // 改变页码
    const onChange = (page) => setPage(page)
    // 编辑或添加班级
    const editClass = (record) => {
        //显示组件
        setOpenAddStudent(true)
        //区分编辑和添加的情况
        if (record) {
            setEditClassInfo(record)
            setEdit(true)
        } else {
            setEditClassInfo({})
            setEdit(false)
        }
    }
    // 删除班级
    const onDelete = async (classId) => {
        let { code } = (await delClass({ classId })).data
        if (code === 0) {
            message.success('删除成功')
        }
        //这里判断了删除最后一条的情况
        if (total == ((page - 1) * 4 + 1) && total !== 1) {
            setPage(page - 1)
        } else {
            setNewClass(!newClass)
        }
    }

    return (
        <Card
            title="班级信息管理"
            extra={
                <Button type="primary" onClick={() => editClass(false)}>
                    添加班级
                </Button>
            }
        >
            <SearchClass 
                setSearch={setSearch} 
                setPage={setPage} 
                setNewClass={() => setNewClass(!newClass)}
            >
            </SearchClass>
            <Table
                style={{ minWidth: '650px' }}
                columns={ClasstableColumns}
                dataSource={tabledata}
                rowKey={(record) => String(record.classId)}
                // 分页器配置
                pagination={{
                    total,
                    pageSize: 4,
                    current: page,
                    onChange
                }}
            />
            {/*antd的Modal不能即时的刷新数据,因此采用三元运算符对modal进行销毁*/}
            {openAddStudent ?
                    <EditClass
                        open={openAddStudent}
                        {/*组件传参*/}
                        setOpen={setOpenAddStudent}
                        setnewClass={() => setNewClass(!newClass)}
                        pageOne={(page) => setPage(page)}
                        editClassInfo={editClassInfo}
                        edit={edit}
                    ></EditClass>
                    : null
            }
        </Card>
    )
}

2.10 富文本编辑器

这里以公告为例实现富文本编辑器的使用,使用的是wangEditor5版本

2.10.1添加路由

先添加一个公告管理的路由,然后要有详情、编辑的二级路由

/router/RouterList.jsx

...
const Notice = lazy(() => import('@/pages/notice/Notion'))
const EditNews = lazy(() => import('@/pages/notice/EditNews'))
const DateilNews = lazy(() => import('@/pages/notice/DateilNews'))
...

const routerList = [{
    ...
    {
        // 公告管理
        path: '/notice',
        element: <LazyRouter element={<Layout />} title="公告管理"></LazyRouter>,
        children: [{
            path: '',
            element: <LazyRouter element={<Notice />}></LazyRouter>
        }, {
            path: 'editnews',
            element: <LazyRouter element={<EditNews />}></LazyRouter>
        }, {
            path: 'dateilnews',
            element: <LazyRouter element={<DateilNews />}></LazyRouter>
        }]
    },
    ...
}]
 
export default routerList

2.10.2 公告展示

写出公告的展示页面,

下面是最基础的展示页面,没有添加其他逻辑

/pages/notice/Notice.jsx

import React, { useState, useEffect } from 'react'
import { Card, Table, Button, Space, message } from 'antd';
import { getNews, delNews } from '@/api/http';
import { useNavigate, useLocation } from 'react-router-dom';

export default function Notion() {
    const navigeate = useNavigate()
    const { state } = useLocation()
    // 表格数据
    const [tabledata, setTableData] = useState([])
    // 分页显示
    const [page, setPage] = useState(1)
    const [total, setTotal] = useState()
    const [newNewsInfo, setNewNewsInfo] = useState(true)
    // 获取信息
    const newsTableData = async () => {
        let { code, list } = (await getNews()).data
        if (code === 0) {
            setTableData(list)
            setTotal(list.length)
        }
    }
    // 改变页码
    const onChange = (page) => {
        setPage(page)
    }
    // 获取最新数据
    useEffect(() => {
        newsTableData();
    }, [page, newNewsInfo])
    
  	//表格
    const NewsTableColumns = [{
        title: '文章标题',
        dataIndex: 'title',
        key: 'tiele',
    }, {
        title: '作者',
        dataIndex: 'author',
        key: 'author',
    }, {
        title: '操作',
        key: 'action',
        render: (_, record) => (
            <Space size="middle">
                <Button }>预览</Button>
                <Button type='primary'>编辑</Button>
                <Button>删除</Button>
            </Space>
        ),
    }];
    
    return (
        <Card
            title="公告管理"
            extra={<Button type="primary">添加公告</Button>}
        >
            <Table
                columns={NewsTableColumns}
                dataSource={tabledata}
                rowKey={(record) => String(record.newsId)}
                // 分页
                pagination={{
                    total,
                    pageSize: 4,
                    current: page,
                    onChange
                }}
            />
        </Card>
    )
}

2.10.3 封装富文本编辑器组件

官网:www.wangEditor.com

  1. 下载安装依赖

    // 下面两个依赖都需要安装 
    npm i @wangeditor/editor  // css样式以及(TS)相关类型声明
    npm i @wangeditor/editor-for-react
    
  2. 在compented组件中创建一个WangEditor.jsx组件

    富文本编辑器更多的使用方式单独总结,这里只展示用到的部分

    官网上有一个基础的dome示例

    import '@wangeditor/editor/dist/css/style.css' // 引入 css
    import React, { useState, useEffect } from 'react'
    // 工具栏和编辑器都要引入
    import { Editor, Toolbar } from '@wangeditor/editor-for-react'
    
    function MyEditor() {
        // editor 实例
        const [editor, setEditor] = useState(null)    
        // 编辑器内容
        const [html, setHtml] = useState('')
        // 工具栏配置
        const toolbarConfig = { }                      
        // 编辑器配置
        const editorConfig = {                        
            placeholder: '请输入内容...',
        }
        // 及时销毁 editor ,重要!
        useEffect(() => {
            return () => {
                if (editor == null) return
                editor.destroy()
                setEditor(null)
            }
        }, [editor])
    
        return (
            <div style={{ border: '1px solid #ccc', zIndex: 100}}>
                <Toolbar
                    editor={editor}
                    defaultConfig={toolbarConfig}
                    mode="default"
                    style={{ borderBottom: '1px solid #ccc' }}
                    />
                <Editor
                    defaultConfig={editorConfig}
                    value={html}
                    onCreated={setEditor}
                    onChange={editor => setHtml(editor.getHtml())}
                    mode="default"
                    style={{ height: '500px', overflowY: 'hidden' }}
                    />
                {/*预览数据*/}
                <div style={{ marginTop: '15px' }}>
                    {html}
                </div>
            </div>
        )
    }
    
    export default MyEditor
    
  3. 根据这个基础dome做一些添加和修改,主要是需要上传图片

    /compented/WangEditor.jsx

    import React, { useState, useEffect } from 'react'
    import '@wangeditor/editor/dist/css/style.css' // 引入 css
    import { Editor, Toolbar } from '@wangeditor/editor-for-react'
    
    export default function WangEditor(props) {
        // 编辑器内容(由父组件传递进来的)
        const { html, setHtml } = props
        // editor 实例
        const [editor, setEditor] = useState(null)
        // 工具栏配置
        const toolbarConfig = {}
        // 编辑器配置
        const editorConfig = {
            placeholder: '请输入内容...',
            // 添加上传图片的配置
            MENU_CONF: {}
        }
        // 上传图片的配置
        editorConfig.MENU_CONF['uploadImage'] = {
            //上传地址
            server: '/api/upload',
            //上传文件名
            fieldName: 'file',
            //头信息(添加token)
            headers: {
                Authorization: 'Bearer ' + sessionStorage.getItem("token"),
            },
            // 上传之前触发
            onBeforeUpload(file) {   
                // file 选中的文件,格式如 { key: file }
                return file
            },
            // 上传进度的回调函数 progress 是 0-100 的数字
            onProgress(progress) {},
            // 自定义插入图片
            customInsert(res, insertFn) {           
                // res 即服务端的返回结果
                let { url } = res
                // 从 res 中找到 url alt href ,然后插入图片
                insertFn(url)
            },
        }
        // 及时销毁 editor ,重要!
        useEffect(() => {
            return () => {
                if (editor == null) return
                editor.destroy()
                setEditor(null)
            }
        }, [editor])
        
        return (
            <div style={{ border: '1px solid #ccc', zIndex: 100 }}>
                {/* 工具栏 */}
                <Toolbar
                    editor={editor}
                    defaultConfig={toolbarConfig}
                    mode="default"
                    style={{ borderBottom: '1px solid #ccc' }}
                />
                {/* 编辑器 */}
                <Editor
                    defaultConfig={editorConfig}
                    value={html}
                    onCreated={setEditor}
                    {/*实现编辑器*/}
                    onChange={editor => setHtml(editor.getHtml())}
                    mode="default"
                    style={{ height: '450px', overflowY: 'hidden' }}
                />
            </div>
        )
    }
    
    

2.10.3 添加公告

在EditNotice.jsx文件内编写添加的逻辑

这是只写了添加的逻辑

/pages/notice/EditNotice.jsx

import {
    Card,
    Space,
    Spin,
    Button,
    Form,
    Input,
    message
} from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { useForm } from 'antd/es/form/Form';
import { useState,useEffect } from 'react';
import { useNavigate,useLocation } from 'react-router-dom';
import { addNews } from '@/api/http'
// 封装的富文本编辑器组件
import WangEditor from '@/compented/WangEditor';

export default function EditNews() {
    const navigate = useNavigate()
    const { state } = useLocation()
    // 表单实例
    const [editNewsForm] = useForm()
    // 等待动画
    const [loading, setloading] = useState(false);
    // 编辑器内容在父组件定义
    const [html, setHtml] = useState('')
    // 表单提交
    const onFinish = async (values) => {
        setloading(true)
        //将富文本编辑器的内容添加到参数中
        values.content = html
        //添加
        let { code } = (await addNews(values)).data
        if (code === 0) {
            message.success('添加成功')
            navigate('/notion')
        } else {
            message.error('添加失败')
        }
        setloading(false)
    }

    return (
        <Spin indicator={<LoadingOutlined />} spinning={loading}>
            <Card title='添加新闻' extra={<Button onClick={() => navigate('/notion')}>返回列表</Button>}>
                <Form
                    labelCol={{ span: 2 }}
                    wrapperCol={{ span: 18 }}
                    form={editNewsForm}
                    onFinish={onFinish}
                    autoComplete='off'
                    validateTrigger={'onBlur'}
                >
                    <Form.Item label="标题" name='title' rules={[{ required: true, message: '请输入标题' }]} >
                        <Input />
                    </Form.Item>
                    <Form.Item label="作者" name='author' rules={[{ required: true, message: '请输入作者' }]}>
                        <Input />
                    </Form.Item>
                    <Form.Item label="内容" name='content'>
                        {/*向编辑器传递内容*/}
                        <WangEditor html={html} setHtml={setHtml} />
                    </Form.Item>
                    <Form.Item label=" " colon={false}>
                        <Space>
                            <Button type='primary' htmlType='submit'>添加</Button>
                            <Button>取消</Button>
                        </Space>
                    </Form.Item>
                </Form>
            </Card>
        </Spin>
    )
}

2.10.4 编辑公告

编辑公告与编辑记录相似,都是给表单一个初始值,但是这里是用到了富文本编辑器,所以要将初始值给到富文本编辑器

下面是添加了编辑逻辑的代码

/pages/notice/EditNotice.jsx

import {
    Card,
    Space,
    Spin,
    Button,
    Form,
    Input,
    message
} from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { useForm } from 'antd/es/form/Form';
import { useState,useEffect } from 'react';
import { useNavigate,useLocation } from 'react-router-dom';
import { addNews, editNews } from '@/api/http'
//富文本编辑器组件
import WangEditor from '@/compented/WangEditor';

export default function EditNews() {
    const navigate = useNavigate()
    const { state } = useLocation()
    // 表单实例
    const [editNewsForm] = useForm()
    // 等待动画
    const [loading, setloading] = useState(false);
    // 编辑器内容
    const [html, setHtml] = useState(state ? state.record.content : '')
    // 编辑新闻初始化
    const record = state ? state.record : null
    const page = state ? state.page : null
    useEffect(() => {
        record ? setHtml(record.content) : null
    }, [record])
    // 表单提交
    const onFinish = async (values) => {
        setloading(true)
        // 添加
        values.content = html
        if (record) {
            // 编辑
            values.newsId = record.newsId
            let { code } = (await editNews(values)).data
            if (code === 0) {
                message.success('修改成功')
                navigate('/notion', { state: { page } })
            }
        } else {
            // 添加
            values.content = html
            let { code } = (await addNews(values)).data
            if (code === 0) {
                message.success('添加成功')
                navigate('/notion')
            } else {
                message.error('添加失败')
            }
        }
        setloading(false)
    }

    return (
        <Spin indicator={<LoadingOutlined />} spinning={loading}>
            <Card title={record ? '编辑新闻' : '添加新闻'} extra={<Button onClick={() => navigate('/notion')}>返回列表</Button>}>
                <Form
                    labelCol={{ span: 2 }}
                    wrapperCol={{ span: 18 }}
                    form={editNewsForm}
                    initialValues={record ? record : null}
                    onFinish={onFinish}
                    autoComplete='off'
                    validateTrigger={'onBlur'}
                >
                    <Form.Item label="标题" name='title' rules={[{ required: true, message: '请输入标题' }]} >
                        <Input />
                    </Form.Item>
                    <Form.Item label="作者" name='author' rules={[{ required: true, message: '请输入作者' }]}>
                        <Input />
                    </Form.Item>
                    <Form.Item label="内容" name='content'>
                        <WangEditor html={html} setHtml={setHtml} />
                    </Form.Item>
                    <Form.Item label=" " colon={false}>
                        <Space>
                            <Button type='primary' htmlType='submit'>{record ? '保存' : '添加'}</Button>
                            <Button>取消</Button>
                        </Space>
                    </Form.Item>
                </Form>
            </Card>
        </Spin>
    )
}

2.10.5 删除记录

删除记录写到了公告展示页面的逻辑中

下面是公告展示的完整代码

/pages/notice/Notice.jsx

import React, { useState, useEffect } from 'react'
import { Card, Table, Button, Space, message } from 'antd';
import { getNews, delNews } from '@/api/http';
import { useNavigate, useLocation } from 'react-router-dom';

export default function Notion() {
    const navigeate = useNavigate()
    const { state } = useLocation()
    // 表格数据
    const [tabledata, setTableData] = useState([])
    // 分页显示
    const [page, setPage] = useState(state ? state.page : 1)
    const [total, setTotal] = useState()
    const [newNewsInfo, setNewNewsInfo] = useState(true)
    // 获取信息
    const newsTableData = async () => {
        let { code, list } = (await getNews()).data
        if (code === 0) {
            setTableData(list)
            setTotal(list.length)
        }
    }
    // 添加文章
    const onAddNews = () => {
        navigeate('/notion/editnews')
    }
    // 改变页码
    const onChange = (page) => {
        setPage(page)
    }
    // 获取最新数据
    useEffect(() => {
        newsTableData();
    }, [page, newNewsInfo])

    // 编辑新闻
    const editNews = (record) => {
        // console.log(record.content);
        navigeate('/notion/editnews', { state: { record, page } })
    }
    // 删除新闻
    const deleteNews = async (newsId) => {
        let { code } = (await delNews({ newsId })).data
        if (code === 0) {
            setNewNewsInfo(!newNewsInfo)
            message.success('删除成功')
        }
    }
    // 查看新闻
    const dateilNews = (newsId) => {
        navigeate('/notion/dateilnews', { state: { newsId } })
    }

    const NewsTableColumns = [{
        title: '文章标题',
        dataIndex: 'title',
        key: 'tiele',
    }, {
        title: '作者',
        dataIndex: 'author',
        key: 'author',
    }, {
        title: '操作',
        key: 'action',
        render: (_, record) => (
            <Space size="middle">
                <Button onClick={() => { dateilNews(record.newsId) }}>预览</Button>
                <Button type='primary' onClick={() => editNews(record)}>编辑</Button>
                <Button onClick={() => deleteNews(record.newsId)}>删除</Button>
            </Space>
        ),
    }];

    return (
        <Card
            title="公告管理"
            extra={<Button type="primary" onClick={onAddNews}>添加公告</Button>}
        >
            <Table
                columns={NewsTableColumns}
                dataSource={tabledata}
                rowKey={(record) => String(record.newsId)}
                // 分页
                pagination={{
                    total,
                    pageSize: 4,
                    current: page,
                    onChange
                }}
            />
        </Card>
    )
}

2.11Excel表格导入导出

2.11.1 下载 xlsx插件

npm i xlsx

2.11.2 实现导入导出

一般是从后台获取数据的,导入后也是保存到后台的,但是这里为了方便直接就地使用了。如果需要与后台对接,再添加逻辑即可

创建了一个Excle导入导出的路由,实现代码如下

/pages/Excle.jsx

import React from 'react';
import { Upload, Button, message, Table } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
// 引入XLSX
import * as XLSX from 'xlsx'
import { useState } from 'react';
import { useEffect } from 'react';
/**
 * Excel 导入:将表格转换成json 数据 ;npm i xlsx
 * Excel 导出:将json数据转换成表格 ;
 *
 * npm : https://www.npmjs.com/package/xlsx
 */
//表格的表头(表头限制了传入的表格格式,这里是为了演示方便)
const columns = [
  {
    title: "姓名",
    dataIndex: 'name',
    key: 'name',
    align: 'center',
  },
  {
    title: "年龄",
    dataIndex: 'age',
    key: 'age',
    align: 'center',
  },
  {
    title: "年级",
    dataIndex: 'grade',
    key: 'grade',
    align: 'center',
  },
]
// Excel组件
const Excel = () => {
  const [fileList, setfileList] = useState([])  //上传文件列表
  const [uploading, setuploading] = useState(false) //上传状态
  const [dataSource, setdataSource] = useState([]) //表格数据
  //读取上传文件信息
  const readFile = () => {
    return new Promise(resolve => {
      // FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
      let reader = new FileReader()
      // 该方法用于将File对象转化为二进制文件
      reader.readAsBinaryString(fileList[0])
      // 当把文件所有数据加载完毕后,把数据传给promise的下一步
      reader.onload = ev => {
        resolve(ev.target.result)
      }
    })
  }
  // 把上传*.xlsx解析为json
  const onChange = async () => {
    setuploading(true)
    // 获取上传excel的文件数据
    let dataBinary = await readFile();
    // 获取工作簿对象
    let workBook = XLSX.read(dataBinary, { type: "binary", cellDates: true });
    // 获取第一个工作表的数据
    let workSheet = workBook.Sheets[workBook.SheetNames[0]];
    // 把工作表数据转为json
    const data = XLSX.utils.sheet_to_json(workSheet);
    setuploading(false)
    setfileList([])
    setdataSource(data)
    //把json传给tableData
  }
  //fileList变化时调用onChange
  useEffect(() => {
     // 初始化时不执行
    if (fileList.length === 0) return;
    onChange()
  }, [fileList])
  //上传组件Upload的props配置
  const props = {
    beforeUpload: file => {
      let arr = file.name.split('.');
      if (arr[arr.length - 1] === 'xlsx') {
        setfileList(() => [file])
      } else {
        message.warn("请选择正确的文件");
      }
      return false;
    },
    fileList,
    maxCount: 1,
  };
  //把json导出为excel
  const handleExport = () => {
    let workbook = XLSX.utils.book_new();
    let workSheet = XLSX.utils.json_to_sheet(dataSource.map(item => {
      delete item.key
      return item
    }));
    // table1为工作表的名字
    XLSX.utils.book_append_sheet(workbook, workSheet, '七二班')
    XLSX.writeFileXLSX(workbook, '外国语中学学生信息.xlsx')
  }

  return (
    <>
      <Upload {...props} disabled={uploading}>
        <Button icon={<UploadOutlined />} disabled={uploading} loading={uploading}>
          {uploading ? '正在导入...' : 'Excel导入'}
        </Button>
      </Upload>
      <Button onClick={handleExport}>导出</Button>
      <Table
        dataSource={dataSource}
        columns={columns}
        pagination={false}
        rowKey={(record)=>record.name}
      />
    </>
  );
}

export default Excel;

3 结束

就这么多吧,项目中遇到的问题再单独总结。

 类似资料: