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' const reducer = combineReducers({ app }) 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 './index.scss' import App from './App'; import store from './store/index';***** import {Provider} from 'react-redux'**** const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider> );
-
在 AppHeader.jsx 中导入 connect 将全局状态的属性和方法获取到,然后使用
import { MenuFoldOutlined, MenuUnfoldOutlined, } from '@ant-design/icons'; import { Layout, } from 'antd'; import React from 'react'; import { connect } from 'react-redux'; const { Header } = Layout; const Headers = ({changeCollapsed,collapsed}) => { return ( <Header style={{ padding: 0, background: '#fff', }} > {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => changeCollapsed(), })} </Header> ); }; export default connect(state => { return { collapsed:state.getIn(['app','collapsed']) } },dispatch => { return { changeCollapsed(){ dispatch({type:'change_collapsed'}) } } })(Headers);
5.进入侧边栏组件通过 connect 拿到是否展开的属性,然后使用即可
import { UploadOutlined, UserOutlined, VideoCameraOutlined, } from '@ant-design/icons'; import { Layout, Menu } from 'antd'; import React from 'react'; import logo from '../../logo.svg' import {connect } from 'react-redux'; const { Sider } = Layout; const Aside = ({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: 14}}>OA后台管理系统</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']) } })(Aside);
路由模式
//App.jsx import React from 'react'; import Index from './layout/Index' import { HashRouter,Switch,Route } from 'react-router-dom'; const App = () => { return ( <HashRouter> <Switch> <Route path='/'component={Index}></Route> </Switch> </HashRouter> ); }; export default App;
登录模块
1. 创建一个 login 页面
进入UI组件库选择表单组件
将表单组件中的内容改成我们所需要的
在 App.jsx 中配置路由处理,加了登录的路由,且添加了一个精确匹配
//App.jsx import React from 'react'; import Index from './layout/Index' import Login from './views/login/Index' import { HashRouter,Switch,Route } from 'react-router-dom'; const App = () => { return ( <HashRouter> <Switch> <Route exact path='/login'component={Login}></Route> <Route path='/'component={Index}></Route> </Switch> </HashRouter> ); }; export default App;
测试登录的事件没问题
将 utils 的 request.js 中关于响应拦截器中的 token 失效的处理注释掉
import axios from 'axios' const isDev = process.env.NODE_ENV === 'development' const request = axios.create({ baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin', timeout: 60000, // 默认值是 `0` (永不超时) }) // 添加请求拦截器 request.interceptors.request.use(function (config) { const token = localStorage.getItem('token') config.headers.token = token return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 request.interceptors.response.use(function (response) { if (response.data.code === '10119') { } return response.data; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); }); export default function ajax (config) { const { url = '', method = 'GET', data = {}, headers = {} } = config switch (method.toUpperCase()) { case 'GET': return request.get(url, { params: data }) case 'POST': 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 }) } 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 }) } 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 } ) }
login/index.jsx 调登录接口
import React from 'react'; import './index.scss' import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { Button, Checkbox, Form, Input } from 'antd'; import { loginFn } from '../../api/user'; const Index = () => { const onFinish = (values) => { loginFn(values).then(res =>{ console.log(res); }) }; return ( <Form></Form> ); }; export default Index;
登录之后的错误处理
轻提示
import React from 'react'; import './index.scss' import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { Button, Checkbox, Form, Input,message, Space} from 'antd'; import { loginFn } from '../../api/user'; const Index = () => { const [messageApi, contextHolder] = message.useMessage(); const onFinish = (values) => { loginFn(values).then(res =>{ console.log(res); if(res.code ==='200'){ messageApi.open({ type:'success', content:res.message }) }else{ messageApi.open({ type:'error', content:res.message }) } }) }; 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 Index;
2. 创建登录之后的用户状态管理
创建一个关于用户的 reducer,store/modules/user.js
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-loginState': return state.set('loginState', payload) case 'change-token': return state.set('token', payload) case 'change-adminname': 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
在 store/index.js 文件中进行配置该 reducer
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
在登录页面 通过 connect 将修改的方法映射到我们的 props 中
在提交表单的时候进行数据请求
3. 登录成功之后的处理
验证用户是否登录成功
失败给出对应的警告提示
成功我们会保存用户信息到本地和全局状态
跳转到首页面
import React from 'react'; import './index.scss' import { LockOutlined, UserOutlined } from '@ant-design/icons'; import { Button, Checkbox, Form, Input,message} from 'antd'; import { useHistory } from 'react-router-dom'; import { connect } from 'react-redux'; import { loginFn } from '../../api/user'; const Index = ({loginDispatch}) => { const history = useHistory() const [messageApi, contextHolder] = message.useMessage(); const onFinish = (values) => { loginDispatch(values,messageApi).then(()=> { //登录完成之后的跳转 history.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 { loginDispatch(values,messageApi){ return new Promise(resolve => { loginFn(values).then(res =>{ console.log(res); if(res.code ==='200'){ messageApi.open({ type:'success', content:res.message }) //存储本地 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-loginState',payload:true}) dispatch({type:'change-token',payload:res.data.token}) dispatch({type:'change-adminname',payload:res.data.adminname}) dispatch({type:'change-role',payload:res.data.role}) dispatch({type:'change-checkedkeys',payload:res.data.checkedkeys}) //告诉已经成功 resolve() }else{ messageApi.open({ type:'error', content:res.message }) } }) }) } } })(Index);
页面创建和路由处理
创建页面
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
aside 中导入menus
import { Layout, Menu } from 'antd';
import menus from '../../router/index'
import React from 'react';
import logo from '../../logo.svg'
import {connect } from 'react-redux';
const { Sider } = Layout;
const Aside = ({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: 14}}>OA后台管理系统</span>)}
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={menus}
/>
</Sider>
);
};
export default connect(state =>{
return {
collapsed:state.getIn(['app','collapsed'])
}
})(Aside);
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;
3 侧边栏默认选中项目
import { Layout, Menu } from 'antd';
import menus from '../../router/index'
import React, { useState } from 'react';
import logo from '../../logo.svg'
import { connect } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
const { Sider } = Layout;
const Aside = ({ collapsed }) => {
const { pathname } = useLocation()
const history = useHistory()
const [openKeys] = useState(['/' + pathname.split('/')[1]])
const [selectedKeys] = useState([pathname])
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: 14 }}>OA后台管理系统</span>)}
</div>
<Menu
defaultOpenKeys={openKeys}
theme="dark"
mode="inline"
defaultSelectedKeys={selectedKeys}
items={menus}
onClick={(item) => {
if (item.key != pathname) {
history.push(item.key)
}
}}
/>
</Sider>
);
};
export default connect(state => {
return {
collapsed: state.getIn(['app', 'collapsed'])
}
})(Aside);
4.退出登录
header.jsx
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
} from '@ant-design/icons';
import { Button, Layout, } from 'antd';
import React from 'react';
import { connect } from 'react-redux';
import { useHistory } from 'react-router-dom';
const { Header } = Layout;
const Headers = ({ changeCollapsed, collapsed,adminname,logout}) => {
const history = useHistory()
return (
<Header
style={{
padding: 0,
background: '#fff',
}}
>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => changeCollapsed(),
})}
<div style={{float:'right',marginRight:30}}>
{adminname}
{adminname? <Button onClick={()=>{
logout()
history.push('/login')
}}>退出</Button> : <Button onClick={()=>{
history.push('/login')
}}>请登录</Button>}
</div>
</Header>
);
};
export default connect(state => {
return {
collapsed: state.getIn(['app', 'collapsed']),
adminname:state.getIn(['user', 'adminname'])
}
}, dispatch => {
return {
changeCollapsed() {
dispatch({ type: 'change_collapsed' })
},
logout(){
localStorage.clear()
dispatch({type:'change-loginState',payload:false})
dispatch({type:'change-token',payload:''})
dispatch({type:'change-adminname',payload:''})
dispatch({type:'change-role',payload:1})
dispatch({type:'change-checkedkeys',payload:[]})
}
}
})(Headers);
商品管理
1. 商品列表
创建 api/pro.js 编写请求接口
找到 Table 组件,将数据渲染到该组件中
该组件中 配置了数据源,每行显示规则,分页等配置
连调数据是否推荐
2.推荐页面
按照同 商品列表相同方式
先请求数据
然后渲染数据即可