react-admin
创建项目安装依赖
npx create-react-app react-admin
安装依赖:
cnpm i axios redux react-redux immutable redux-immutable redux-thunk node-sass react-router@5 react-router-dom@5 antd
将项目架构搭建起来
-
将所需要的 layout 组件拿过来
-
在 App.jsx 中复制刚选中的 layout 布局
// App.jsx
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu, theme } from 'antd';
import React, { useState } from 'react';
import './App.scss'
const { Header, Sider, Content } = Layout;
const App = () => {
const [collapsed, setCollapsed] = useState(false);
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<Layout id='components-layout'>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: 'nav 1',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: 'nav 2',
},
{
key: '3',
icon: <UploadOutlined />,
label: 'nav 3',
},
]}
/>
</Sider>
<Layout className="site-layout">
<Header
style={{
padding: 0,
background: colorBgContainer,
}}
>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
</Header>
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
}}
>
Content
</Content>
</Layout>
</Layout>
);
};
export default App;
- 创建一个 index.scss 用来放全局样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
list-style: none;
text-decoration: none;
// 设置网页内容无法被选中
user-select: none;
}
html, body, #root,#components-layout {
height: 100%;
}
#components-layout .trigger {
padding: 0 24px;
font-size: 18px;
line-height: 64px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout .trigger:hover {
color: #1890ff;
}
#components-layout .logo {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}
- 在入口的 Index.js 文件中导入 index.scss文件
- logo 处理
导入 logo , import logo from './logo.svg'
添加标题和 logo
<div className="logo"> <img style={{height:30}} src={logo} alt="" /> {!collapsed && (<span style={{lineHeight: '32px', color: '#fff', fontSize: 18}}>嗨购管理系统</span>)} </div>
页面处理
app.jsx
// App.jsx
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu, theme } from 'antd';
import React, { useState } from 'react';
import './App.scss'
// 导入小图标
import logo from './logo.svg'
const { Header, Sider, Content } = Layout;
const App = () => {
// 是否为打开
const [collapsed, setCollapsed] = useState(false);
return (
<Layout id='components-layout'>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo">
<img style={{height:30}} src={logo} alt="" />
{!collapsed && (<span style={{lineHeight: '32px', color: '#fff', fontSize: 18}}>嗨购管理系统</span>)}
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: '首页',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: '账号管理',
},
{
key: '3',
icon: <UploadOutlined />,
label: '轮播图管理',
},
]}
/>
</Sider>
<Layout className="site-layout">
<Header
style={{
padding: 0,
background: '#fff',
}}
>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
</Header>
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: '#fff',
}}
>
Content
</Content>
</Layout>
</Layout>
);
};
export default App;
1. 拆分 layout
1. 头部
// AppHeader
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
} from '@ant-design/icons';
import { Layout } from 'antd';
import React, { useState } from 'react';
const { Header } = Layout;
const AppHeader = () => {
// 是否为打开
const [collapsed, setCollapsed] = useState(false);
return (
<Header
style={{
padding: 0,
background: '#fff',
}}
>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
</Header>
);
//
};
export default AppHeader;
2.侧边栏
// SiderBar
import {
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import React, { useState } from 'react';
// 导入小图标
import logo from '../../logo.svg'
const { Sider } = Layout;
const SiderBar = () => {
// 是否为打开
const [collapsed] = useState(false);
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo">
<img style={{height:30}} src={logo} alt="" />
{!collapsed && (<span style={{lineHeight: '32px', color: '#fff', fontSize: 18}}>嗨购管理系统</span>)}
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: '首页',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: '账号管理',
},
{
key: '3',
icon: <UploadOutlined />,
label: '轮播图管理',
},
]}
/>
</Sider>
);
};
export default SiderBar;
3. 内容
// AppMain
import { Layout } from 'antd';
import React from 'react';
const { Content } = Layout;
const AppMain = () => {
return (
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: '#fff',
}}
>
Content
</Content>
);
};
export default AppMain;
4. 收集导出组件
// 用来导入收集组件用的
export { default as AppHeader } from './AppHeader'
export { default as AppMain } from './AppMain'
export { default as SiderBar } from './SiderBar'
5. Index.jsx 显示组件
import { Layout } from 'antd';
import React from 'react';
// 导入所有组件
import {AppHeader,AppMain,SiderBar} from './components/index'
const App = () => {
return (
<Layout id='components-layout'>
<SiderBar />
<Layout className="site-layout">
<AppHeader />
<AppMain />
</Layout>
</Layout>
);
};
export default App;
2. 全局状态管理
- 先在 src/store/index.js 创建 store
import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { combineReducers } from 'redux-immutable'
import app from './modules/app'
import user from './modules/user'
const reducer = combineReducers({
app,
user
})
const store = createStore(reducer, applyMiddleware(thunk))
export default store
- 在 src/store/modules/app.js 创建关于侧边栏是否展开状态的 reducer
import { Map } from 'immutable'
const reducer = (state = Map({
// 监听侧边栏是否展开
collapsed: localStorage.getItem('collapsed') === 'true'
}), action) => {
switch (action.type) {
case 'change-collapsed':
// 将全局状态的值取反,然后保存
localStorage.setItem('collapsed', !state.get('collapsed'))
return state.set('collapsed', !state.get('collapsed'))
default:
return state
}
}
export default reducer
-
入口 index.js 文件中配置 store
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import store from './store/index' import { Provider } from 'react-redux' // 导入全局样式 import './index.scss' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider> ); -
在 AppHeader.jsx 中导入 connect 将全局状态的属性和方法获取到,然后使用
// AppHeader import { MenuFoldOutlined, MenuUnfoldOutlined, } from '@ant-design/icons'; import { Layout } from 'antd'; import React from 'react'; import { connect } from 'react-redux' const { Header } = Layout; const AppHeader = ({collapsed, setCollapsed}) => { return ( <Header style={{ padding: 0, background: '#fff', }} > {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => setCollapsed(!collapsed), })} </Header> ); // }; export default connect(state => { return { collapsed: state.getIn(['app', 'collapsed']) } }, dispacth => { return { setCollapsed(){ dispacth({type: 'change-collapsed'}) } } })(AppHeader); -
进入侧边栏组件通过 connect 拿到是否展开的属性,然后使用即可
// SiderBar import { UploadOutlined, UserOutlined, VideoCameraOutlined, } from '@ant-design/icons'; import { Layout, Menu } from 'antd'; import React from 'react'; import {connect} from 'react-redux' // 导入小图标 import logo from '../../logo.svg' const { Sider } = Layout; const SiderBar = ({collapsed}) => { return ( <Sider trigger={null} collapsible collapsed={collapsed}> <div className="logo"> <img style={{height:30}} src={logo} alt="" /> {!collapsed && (<span style={{lineHeight: '32px', color: '#fff', fontSize: 18}}>嗨购管理系统</span>)} </div> <Menu theme="dark" mode="inline" defaultSelectedKeys={['1']} items={[ { key: '1', icon: <UserOutlined />, label: '首页', }, { key: '2', icon: <VideoCameraOutlined />, label: '账号管理', }, { key: '3', icon: <UploadOutlined />, label: '轮播图管理', }, ]} /> </Sider> ); }; export default connect(state=> { return { collapsed: state.getIn(['app', 'collapsed']) } },null)(SiderBar)
登录模块
1. 创建一个 login 页面
进入UI组件库选择表单组件
将表单组件中的内容改成我们所需要的
在 App.jsx 中配置路由处理,加了登录的路由,且添加了一个精确匹配
import React from 'react'; import Index from './layout/Index' import Login from './vuews/login/Index' // HashRouter, BrowserRouter import { HashRouter, Switch, Route } from 'react-router-dom' const App = () => { return ( <HashRouter> {/* V6 路由配置 */} {/* <Routes> <React path='/' element={<Index />} ></React> </Routes> */} {/* V5 路由配置 */} <Switch> {/* exact: 配置精确匹配路由,防止 login 会进入 首页 */} <Route exact path='/' component={Index} ></Route> <Route exact path='/login' component={Login} ></Route> </Switch> </HashRouter> ); }; export default App;测试登录的事件没问题
import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { Button, Checkbox, message, Form, Input } from 'antd'; import {loginFn} from '../../api/user' import './index.scss' import { connect } from 'react-redux'; // 在 react-router 和 react-router-dom 都可以解构 useHistory // import { useHistory } from 'react-router'; import { useHistory } from 'react-router-dom'; const App = ({login}) => { const [messageApi, contextHolder] = message.useMessage(); const router = useHistory() const onFinish = (values) => { // 表单的提交事件 // console.log('Received values of form: ', values); login(values,messageApi).then(()=>{ // 登录完成之后的处理 // 跳转到首页 router.push('/') }) }; return ( <> {contextHolder} <Form name="normal_login" className="login-form" initialValues={{ remember: true, }} onFinish={onFinish} > <Form.Item name="adminname" rules={[ { required: true, message: '管理员账号不能为空!', }, ]} > <Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="请输入管理员名称" /> </Form.Item> <Form.Item name="password" rules={[ { required: true, message: '密码不能为空!', }, ]} > <Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="请输入管理员密码" /> </Form.Item> <Form.Item> <Form.Item name="remember" valuePropName="checked" noStyle> <Checkbox>记住密码</Checkbox> </Form.Item> <a className="login-form-forgot" href=" "> 忘记密码 </a> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" className="login-form-button"> 登录 </Button> 或 <a href=" ">去注册!</a> </Form.Item> </Form> </> ); }; export default connect(null, dispatch => { return { login(values,messageApi){ return new Promise(resolve => { loginFn(values).then(res => { // console.log(res); if(res.code === '200'){ // console.log('成功'); messageApi.open({ type: 'success', content: '恭喜你登录成功了!', }); // 将用户的登录状态存到本地 localStorage.setItem('loginState', true) localStorage.setItem('token', res.data.token) localStorage.setItem('adminname', res.data.adminname) localStorage.setItem('role', res.data.role) localStorage.setItem('checkedkeys', JSON.stringify(res.data.checkedkeys)) // 将用户状态存到全局状态 dispatch({type: 'change-login-state', payload: true}) dispatch({type: 'change-token', payload: res.data.token}) dispatch({type: 'change-admin-name', payload: res.data.adminname}) dispatch({type: 'change-role', payload: res.data.role}) dispatch({type: 'change-checkedkeys', payload: res.data.checkedkeys}) // 告诉展示组件我们登录处理完成了,你可以进行跳转到想要去的页面 resolve() }else { console.log('失败'); messageApi.open({ type: 'error', content: '温馨提示:' + res.message, }); } }) }) } } })(App);将 utils 的 request.js 中关于响应拦截器中的 token 失效的处理注释掉
// src/utils/request.js // 该文件是用来 二次 封装 axios 用的 import axios from 'axios' // 开发环境(写代码的阶段) development // 生产环境(代码已经写好已经部署到服务器了) production // 测试环境(代码已经写好了但是还没有部署) production // process.env.NODE_ENV 用来获取我们当前的环境 const isDev = process.env.NODE_ENV === 'development' // 创建一个请求对象 const request = axios.create({ // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。 // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin', // baseURL: 'http://121.89.205.189:3000/admin', // `timeout` 指定请求超时的毫秒数。 // 如果请求时间超过 `timeout` 的值,则请求会被中断 timeout: 60000, // 默认值是 `0` (永不超时) }) // 添加请求拦截器 request.interceptors.request.use(function (config) { // 在发送请求之前做些什么 // config 请求配置,里面有请求的参数,请求的 url // console.log(localStorage.getItem('token')); // 1. 先获取 token const token = localStorage.getItem('token') // 2. 设置 token config.headers.token = token return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 request.interceptors.response.use(function (response) { // 2xx 范围内的状态码都会触发该函数。 // 对响应数据做点什么 // response 是我们请求之后的结果 // console.log('111111111111111', response.data.code); if (response.data.code === '10119') { // token 无效,之后我们可以重新登录 // localStorage.clear() // location.href = '/#/login' } return response.data; }, function (error) { // 超出 2xx 范围的状态码都会触发该函数。 // 对响应错误做点什么 return Promise.reject(error); }); // ajax 自定义各种数据请求的方法 export default function ajax (config) { // 1. 先获取到请求的一些必要参数 const { url = '', method = 'GET', data = {}, headers = {} } = config // 2. 判断我们请求的类型是 get 还是 post 还是其他 switch (method.toUpperCase()) { case 'GET': // get 请求 // get 请求的参数我们需要放在 params 中 return request.get(url, { params: data }) case 'POST': // post 请求 // 1. 表单提交数据 if (headers['content-type'] === 'application/x-www-form-url-encoded') { // 如果提交的是表单数据我们需要格式化数据 const obj = new URLSearchParams() for (const key in data) { obj.append(key, data[key]) } return request.post(url, obj, { headers }) } // 2. 文件数据 if (headers['content-type'] === 'multipart/form-data') { const obj = new FormData() for (const key in data) { obj.append(key, data[key]) } return request.post(url, obj, { headers }) } // 3. json 数据 return request.post(url, data) case 'PUT': // 通常用来修改数据用的 --- 数据更新 return request.put(url, data) case 'DELETE': // 删除数据 return request.delete(url, { data }) case 'PATCH': // 更新局部资源 return request.patch(url, data) default: // 如果前面全部都不是 return request.request(config) } }创建 api/user.js 文件封装一个登录的请求接口
import axios from "../utils/request"; // 管理员系统登录接口的封装 export function loginFn (data) { return axios({ url: '/admin/login', method: 'post', data }) }
2. 创建登录之后的用户状态管理
创建一个关于用户的 reducer,store/modules/user.js
在 store/index.js 文件中进行配置该 reducer
在登录页面 通过 connect 将修改的方法映射到我们的 props 中
在提交表单的时候进行数据请求
3. 登录成功之后的处理
验证用户是否登录成功
失败给出对应的警告提示
成功我们会保存用户信息到本地和全局状态
import { Map } from "immutable"; const reducer = (state = Map({ loginState: localStorage.getItem('loginState') === 'true', token: localStorage.getItem('token') || '', adminname: localStorage.getItem('adminname') || '', role: localStorage.getItem('role') * 1 || 1, checkedkeys: JSON.parse(localStorage.getItem('checkedkeys')) || [], }), { type, payload }) => { switch (type) { case 'change-login-state': return state.set('loginState', payload) case 'change-token': return state.set('token', payload) case 'change-admin-name': return state.set('adminname', payload) case 'change-role': return state.set('role', payload) case 'change-checkedkeys': return state.set('checkedkeys', payload) default: return state } } export default reducer跳转到首页面
页面创建和路由处理
创建页面
banner
error
home
login
pro
user
1.创建 src/router/index.js 配置路由规则
import React from 'react'
import {
UploadOutlined,
UserOutlined,
OrderedListOutlined
} from '@ant-design/icons';
const menus = [
{
key: '/',
icon: <UserOutlined />,
label: '系统首页',
component: React.lazy(() => import('../vuews/home/Index'))
},
{
key: '/user',
icon: <UserOutlined />,
label: '账号管理',
children: [
{
key: '/user/list',
icon: <UserOutlined />,
label: '用户列表',
component: React.lazy(() => import('../vuews/user/Index'))
}, {
key: '/user/admin',
icon: <UserOutlined />,
label: '管理员列表',
component: React.lazy(() => import('../vuews/user/Admin'))
}
]
},
{
key: '/banner',
icon: <UploadOutlined />,
label: '轮播图管理',
children: [
{
key: '/banner/list',
icon: <UserOutlined />,
label: '轮播图列表',
component: React.lazy(() => import('../vuews/banner/Index'))
}, {
key: '/banner/add',
icon: <UserOutlined />,
label: '添加轮播图',
component: React.lazy(() => import('../vuews/banner/Add'))
}
]
}, {
key: '/pro',
icon: <OrderedListOutlined />,
label: '商品管理',
children: [
{
key: '/pro/list',
icon: <UserOutlined />,
label: '商品列表',
component: React.lazy(() => import('../vuews/pro/Index'))
}, {
key: '/pro/search',
icon: <UserOutlined />,
label: '筛选列表',
component: React.lazy(() => import('../vuews/pro/Search'))
}, {
key: '/pro/recommend',
icon: <UserOutlined />,
label: '推荐列表',
component: React.lazy(() => import('../vuews/pro/Recommend'))
}, {
key: '/pro/seckill',
icon: <UserOutlined />,
label: '秒杀列表',
component: React.lazy(() => import('../vuews/pro/Seckill'))
}
]
},
]
export default menus
2. 在 AppMain.jsx 中进行路由配置
// AppMain
import { Layout } from 'antd';
import React from 'react';
// 路由出口和路由配置
import { Switch, Route } from 'react-router-dom'
// 路由的规则
import menus from '../../router';
const { Content } = Layout;
const AppMain = () => {
// 渲染路由的函数
const renderRoute = (menus)=>{
return menus.map(item => {
if(item.children){
// 有子路由
return renderRoute(item.children)
}else {
// 无子路由
return <Route exact key={item.key} path={item.key} component={item.component} />
}
})
}
return (
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: '#fff',
}}
>
{/* 配置路由 */}
<React.Suspense fallback={<div>加载中...</div>}>
<Switch>
{renderRoute(menus)}
</Switch>
</React.Suspense>
</Content>
);
};
export default AppMain;
商品管理
1. 商品列表
创建 api/pro.js 编写请求接口
找到 Table 组件,将数据渲染到该组件中
该组件中 配置了数据源,每行显示规则,分页等配置
连调数据是否推荐
2.推荐页面
按照同 商品列表相同方式
先请求数据
然后渲染数据即可
3. 查询功能
添加一个下拉选择框和输入框以及一个查询按钮
点击查询按钮发送数据请求将内容传递过去,拿到查询后的结果然后显示
轮播图管理
1. 轮播图列表
api 封装请求到用户数据
将用户数据渲染出来
2. 添加轮播图
点击添加时调用添加接口,将输入的内容和选中的图片传递到服务器
添加成功后清除数据,跳转到轮播图列表页面
账号管理
用户列表
api 封装请求到用户数据
将用户数据渲染出来
管理员列表
- 请求管理员列表
- 将管理员数据渲染出来
- 添加管理员的抽屉效果
- 编辑管理员的弹出对话框效果
- 删除管理员功能实现
- 在关闭对话框和抽屉时清除表单数据
- 自定义 hooks 深拷贝 menus
图表使用
npm install @ant-design/charts --save
import React from 'react';
import { DecompositionTreeGraph } from '@ant-design/graphs';
const DemoDecompositionTreeGraph = () => {
const data = {
id: 'A0',
value: {
title: 'H5-2218',
items: [
{
text: '1000万',
},
],
},
children: [
{
id: 'A1',
value: {
title: '北京',
items: [
{
text: '200万',
},
{
text: '占比',
value: '20%',
},
],
},
children: [
{
id: 'A11',
value: {
title: '海淀',
items: [
{
text: '100万',
},
{
text: '占比',
value: '50%',
},
],
},
},
{
id: 'A12',
value: {
title: '朝阳',
items: [
{
text: '50万',
},
{
text: '占比',
value: '25%',
},
],
},
},
{
id: 'A13',
value: {
title: '昌平',
items: [
{
text: '50万',
},
{
text: '占比',
value: '25%',
},
],
},
},
],
},
{
id: 'A2',
value: {
title: '上海',
items: [
{
text: '500万',
},
{
text: '占比',
value: '50%',
icon: 'https://gw.alipayobjects.com/zos/antfincdn/iFh9X011qd/7797962c-04b6-4d67-9143-e9d05f9778bf.png',
},
],
},
children: [
{
id: 'A21',
value: {
title: '浦东新区',
items: [
{
text: '200万',
},
{
text: '占比',
value: '40%',
},
],
},
children: [
{
id: 'A211',
value: {
title: '浦东新区北部',
items: [
{
text: '100万',
},
{
text: '占比',
value: '50%',
},
],
},
},
{
id: 'A212',
value: {
title: '浦东新区南部',
items: [
{
text: '100万',
},
{
text: '占比',
value: '50%',
},
],
},
}
]
},
{
id: 'A22',
value: {
title: '闵行区',
items: [
{
text: '300万',
},
{
text: '占比',
value: '60%',
},
],
},
}
]
},
],
};
const config = {
data,
markerCfg: (cfg) => {
const { children } = cfg;
return {
show: children?.length,
};
},
behaviors: ['drag-canvas', 'zoom-canvas', 'drag-node'],
onReady: (graph) => {
graph.on('node:click', (evt) => {
console.log(evt);
});
}
};
return <DecompositionTreeGraph {...config} />;
};
export default DemoDecompositionTreeGraph
富文本编辑器
cnpm i braft-editor
import React, { useState } from 'react';
import BraftEditor from 'braft-editor'
import 'braft-editor/dist/index.css'
import { Button } from 'antd';
const Index = () => {
const [htmlStr, setHtmlStr] = useState()
const handleEditorChange = (editorState) => {
// console.log(editorState.toHTML());
setHtmlStr(editorState.toHTML())
}
return (
<div>
<h3>富文本编辑</h3>
<Button onClick={()=>{
console.log(htmlStr);
}}> 获取输入内容 </Button>
<BraftEditor onChange={handleEditorChange} />
</div>
);
};
export default Index;
MD 编辑器
npm install react-markdown-editor-lite --save
npm install markdown-it
import React from 'react';
// 解析 MD 语法用的
import MarkdownIt from 'markdown-it';
// 用来编辑 MD 的组件
import MdEditor from 'react-markdown-editor-lite';
// import style manually
import 'react-markdown-editor-lite/lib/index.css';
const Md = () => {
const mdParser = new MarkdownIt(/* Markdown-it options */);
function handleEditorChange({ html, text }) {
// 当编辑器内容发生改变时的回调函数
console.log('handleEditorChange', html, text);
}
return (
<div>
<h3>MD 编辑器</h3>
<MdEditor style={{ height: '500px' }} renderHTML={text => mdParser.render(text)} onChange={handleEditorChange} />
</div>
);
};
export default Md;
导入
cnpm i xlsx
import React, { useState } from 'react';
import { Input, Table } from 'antd';
import * as XLSX from 'xlsx'
const Import = () => {
const [proList, setProList] = useState()
// 选中文件之后会回调该函数
function upload(e){
console.log(e.target.files[0]);
const reader = new FileReader()
// 将文件读取成数据流
reader.readAsBinaryString(e.target.files[0])
reader.onload = function(){
// 将文件数据流读取成 js 对象
const boox = XLSX.read(reader.result, {type: 'binary'})
// 在对象中获取 工作表1 中的内容
let res = boox.Sheets['工作表1']
// 将表内容转换成 json 数据
res = XLSX.utils.sheet_to_json(res)
// console.log(res);
setProList(res)
}
}
const columns = [
{
title: '序号',
render(t,r,i){
return <span>{i + 1}</span>
}
}, {
title: '商品名称',
dataIndex: 'proname'
}
]
return (
<div>
<h3>数据导入</h3>
<Input type='file' onChange={upload} />
<Table dataSource={proList} rowKey='proname' pagination={{pageSize: 8}} columns={columns} />
</div>
);
};
export default Import;
导出
npm i js-export-excel
import React, { useEffect, useState } from 'react';
import {getProList} from '../../api/pro'
import { Button, Table } from 'antd';
import ExportJsonExcel from 'js-export-excel'
const Index = () => {
const [proList,setProList] = useState()
useEffect(()=>{
getProList().then(res => {
console.log(res);
setProList(res.data)
})
}, [])
const columns = [
{
title: '序号',
render(t,r,i){
return <span>{i + 1}</span>
}
}, {
title: '商品名称',
dataIndex: 'proname'
}
]
return (
<div>
<h3>文件导出</h3>
<Button onClick={()=>{
const options = {
fileName: 'proList',
datas: [
{
sheetData: proList,
sheetName: "你笑起来真好看",
sheetFilter: ["brand", "category", 'desc' ,'proname', 'originprice', 'img1'],
sheetHeader: ["品牌", "分类", '描述', '商品名称', '价格', '图片'],
columnWidths: [],
}
]
}
var toExcel = new ExportJsonExcel(options); //new
toExcel.saveExcel(); //保存
}}>导出</Button>
<Table dataSource={proList} rowKey='proname' pagination={{pageSize: 8}} columns={columns} />
</div>
);
};
export default Index;
高德地图
npm i @amap/amap-jsapi-loader --save
导入地图import AMapLoader from '@amap/amap-jsapi-loader';
在页面中创建一个用来显示地图的 div 且添加一个 id 叫 container,注意要添加一个高度
useEffect(()=>{
AMapLoader.load({
key: '81a034c1d8522e4db17d8a854004d1f5', // 申请好的Web端开发者Key,首次调用 load 时必填
version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: ['AMap.ToolBar','AMap.Scale','AMap.CitySearch'], // 需要使用的插件列表,如比例尺'AMap.Scale'等
})
.then(AMap => {
const map = new AMap.Map('container', {
// 设置地图容器id
viewMode: '3D', // 是否为3D地图模式
//pitch:75, // 地图俯仰角度,有效范围 0 度- 83 度
zoom: 12, // 初始化地图级别
center: [116.40944792968753, 39.901030607457564], // 初始化地图中心点位置
terrain: true, // 开启地形图
mapStyle: 'amap://styles/0527458982faa051cc0bd48055544ba8',//设置地图的显示样式
});
})
.catch(e => {
console.log(e);
});
面包屑
先在 layout 中创建一个 面包屑组件
在 layout /components/index.js 导入面包屑
在 layout/Index.jsx 导入 ,然后将面包屑放在头部和内容的中间
import { HomeOutlined } from '@ant-design/icons';
import { Breadcrumb } from 'antd';
import React from 'react';
import { useLocation } from 'react-router';
import menus from '../../router';
// 用来放路由的 key 和 value
const breadcrumbNameMap = {}
function getMenusKeyValue(menus){
menus.forEach(item => {
if(item.children){
getMenusKeyValue(item.children)
}
breadcrumbNameMap[item.key] = {
icon: item.icon,
title: item.label
}
});
}
getMenusKeyValue(menus)
console.log(breadcrumbNameMap)
const App = () => {
const {pathname} = useLocation()
// console.log('/' + pathname.split('/')[1]);
// console.log(pathname);
// console.log(breadcrumbNameMap['/' + pathname.split('/')[1]]);
// console.log(breadcrumbNameMap[pathname]);
const items = [
{
href: '/',
title: (<>
<HomeOutlined />
<span>系统首页</span>
</>),
}, {
href: '/' + pathname.split('/')[1],
title: (<>
{breadcrumbNameMap['/' + pathname.split('/')[1]].icon}
<span>{breadcrumbNameMap['/' + pathname.split('/')[1]].title}</span>
</>)
}, {
href: pathname,
title: (<>
{breadcrumbNameMap[pathname].icon}
<span>{breadcrumbNameMap[pathname].title}</span>
</>)
}
]
return (
<Breadcrumb
style={{marginLeft: 20, marginTop: 20}}
items={items}
/>
);
}
export default App;