使用react+antd实现中后台管理项目一些简单的项目功能
//创建项目
pnpm create vite
//安装依赖
pnpm i
//启动项目
pnpm run dev
在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/, ""),
}
}
}
})
//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
main.jsx
import "normalize.css"
antd组件库是按需加载,只需要先把css样式引入到根组件中,在组件中使用时按需引入
App.js
import 'antd/dist/reset.css';
在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/, ""),
}
}
}
})
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>
)
}
先创建好基础的目录,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
}
创建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;
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
})
}
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 >
)
}
在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>
)
}
创建创建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
这个功能可以写成可复用的组件,在根目录创建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
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}
/>
)
}
创建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>
)
}
用到了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
创建对应的文件,并在路由列表内添加信息管理的路由,添加班级管理的二级路由
/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
首先完成最基础的班级列表的展示
/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>
)
}
创建一个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>
)
}
编辑记录跟添加记录使用的同一个组件,唯一的区别是,编辑记录是传递了初始值,然后渲染到页面上,
下面是添加了编辑逻辑的代码
/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>
)
}
查询记录也是采用了组件的方式,这里是后台直接会根据传递的参数返回对应的结果,所以涉及的主要是组件传参
在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>
)
}
删除记录比较简单,只需要一个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>
)
}
这里以公告为例实现富文本编辑器的使用,使用的是wangEditor5版本
先添加一个公告管理的路由,然后要有详情、编辑的二级路由
/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
写出公告的展示页面,
下面是最基础的展示页面,没有添加其他逻辑
/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>
)
}
下载安装依赖
// 下面两个依赖都需要安装
npm i @wangeditor/editor // css样式以及(TS)相关类型声明
npm i @wangeditor/editor-for-react
在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
根据这个基础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>
)
}
在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>
)
}
编辑公告与编辑记录相似,都是给表单一个初始值,但是这里是用到了富文本编辑器,所以要将初始值给到富文本编辑器
下面是添加了编辑逻辑的代码
/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>
)
}
删除记录写到了公告展示页面的逻辑中
下面是公告展示的完整代码
/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>
)
}
npm i xlsx
一般是从后台获取数据的,导入后也是保存到后台的,但是这里为了方便直接就地使用了。如果需要与后台对接,再添加逻辑即可
创建了一个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;
就这么多吧,项目中遇到的问题再单独总结。