点餐后台管理系统(react)
- 一、前言
- 二、项目介绍
- 三、相关技术
- 四、项目实现的功能
-
- 4.1、功能分析
- 4.2、项目结构
- 4.3、axios封装及mock数据
-
- 4.3.1、axios封装
- 4.3.2、mock数据
- 4.4、增删改查实现
-
- 4.4.1、用户列表 (mock数据)
- 4.4.2、通讯信息 (封装axios+antd组件)
- 4.4.3、商户列表 (async+await)
- 4.4.4、登录账户 (hooks)
- 4.4.5、订单列表 (reduX)
- 4.5、登录功能实现
- 4.6、权限处理
- 4.7、echarts数据可视化处理
-
- 4.7.1、用户管理模块(注册用户性别占比分析)
- 4.7.2、用户管理模块(注册用户年龄分析)
- 4.7.3、商户管理模块(商户经营餐饮分析)
- 4.7.4、商户管理模块(商户登录分析)
- 五、项目总结
一、前言
之前也写过一个vue项目的博客,说实话确实写的一般,不过我也是尽我所能去写了。今天,我们用react写一个后台管理系统。依然是新手入门版,大佬可以划走了。
首先,我们要明确一个概念,什么是后台管理系统?
简单概括,后台管理系统是相对于前台系统(也就是我们常见的应用等)而言的,如美团APP,就有相应的后台管理系统,负责商家管理等等。
vue和react作为现如今前端最火的两大框架,无疑是前端开发人员必学的内容之一。
温馨提示,在阅读本篇博客前,你必须掌握react的相关知识,不然你会一脸懵,react可以去我之前的博客学习!
二、项目介绍
本次的点餐后台管理系统是基于react实现的,是为了方便管理商家外卖业务而实现的管理系统,主要模块有 用户管理,商户管理,订单管理,收支管理,平台管理,安全管理,登录
等模块。
由于时间有限,本次项目只实现了用户管理,商户管理,订单管理,登录模块(功能其实差不多)。
项目静态页面一览:
- 主页面
- 登录页
三、相关技术
通过create-react-app
创建官方脚手架,使用类组件
创建布局组件layouts,通过第三方组件库antd
快速实现侧边导航栏的静态结构,使用props
实现父子组件通信,state
定义组件内部初始值,通过onChange()
事件函数实现react表单元素的双向绑定,利用生命周期钩子函数
获取组件初始状态,context状态树传参
,
react-router
实现组件跳转,二级路由,三极路由等,通过hooks
让函数组件操作state数据,redux
进行状态管理,类似vue中的vueX,第三方库echarts
实现数据可视化展示等。
以上基本就是本次项目用到的技术。
四、项目实现的功能
4.1、功能分析
在用户管理,商户管理,订单管理模块中,主要是通过对数据的增删改查实现用户,商户,订单信息的管理及分析。
三个大模块,共5套数据的增删改查,分别使用了不同的技术实现(干货满满,还不收藏吗)
模块 使用的技术用户管理模块:1、用户列表 (mock数据)2、通讯信息 (封装axios+antd组件)
商户管理模块:3、商户列表 (async+await)4、登录账户 (hooks)
订单管理模块:5、订单列表 (reduX)
注意:由于本次项目没有后端人员,所有的接口都是通过mock数据实现的。
4.2、项目结构
项目结构分析:
1)、App.js是一级路由配置,用于登录页面和布局页面的跳转;
class App extends React.Component {constructor(props){super(props); }render = () => (<div className="App"><BrowserRouter><Switch><Redirect exact={true} from="/" to="/login"></Redirect><Route path="/layouts" component={Layouts}></Route><Route path="/login" component={Login}></Route></Switch> </BrowserRouter></div>)
}
2)、在布局组件layouts.js中是二级路由配置,动态显示内容区;
import './index.css'
import {withRouter} from 'react-router-dom'
import Header from '../components/header/header'
import Aside from '../components/aside/aside'
import Userlist from '../pages/Userlist/userlist'
import Commuinfo from '../pages/commuinfo/commuinfo'
import Userstati from '../pages/userstati/userstati'
import Merchanlist from '../pages/merchanlist/merchanlist'
import Loginaccou from '../pages/loginaccou/loginaccou'
import Orderlist from '../pages/orderlist/orderlist'
import Merchans from '../pages/merchans/merchans'
import Orderstati from '../pages/orderstati/orderstati'
import Notfound from '../pages/notfound/notfound'
import {Route,Switch,Redirect} from 'react-router-dom'function Layouts(props){return (<div className="admin_box"><div className="admin_top_box"><Header></Header></div><div className="admin_bottom_box"><div className="admin_left"><Aside></Aside></div><div className="admin_right">{/* Switch排他性匹配 */}<Switch>{/* exact严格模式 ,Redirect重定向*/}<Redirect exact={true} from="/layouts" to={JSON.parse(localStorage.getItem('paths'))[0].path}></Redirect><Route path="/layouts/userlist" component={Userlist}></Route><Route path="/layouts/commuinfo" component={Commuinfo}></Route><Route path="/layouts/userstati" component={Userstati}></Route><Route path="/layouts/merchanlist" component={Merchanlist}></Route><Route path="/layouts/loginaccou" component={Loginaccou}></Route> <Route path="/layouts/orderlist" component={Orderlist}></Route> <Route path="/layouts/merchas" component={Merchans}></Route> <Route path="/layouts/orderstati" component={Orderstati}></Route> {/* 一定能匹配到 */}<Route component={Notfound}></Route></Switch></div></div></div>)
}//高阶组件,获取路由上下文
export default withRouter(Layouts)
4.3、axios封装及mock数据
4.3.1、axios封装
在开始实现增删改查之前,我们要先对axios进行封装,不然下面的代码可能有些看不懂。axios的封装我之前的博客有些过,可以点击这里学习。
本次项目中,由于实现的功能类似(增删改查),封装的axios其实差不多,具体的可以参考以下实例代码:
import {get,post,remove,update} from './index'//查询
export function getUserList(params){//无参数,查询所有数据 ,有参则按照参数查找return get('/userlist',params)
}//删除(通过id删除)
export function removeUserList(params) {//params 要删除数据的idreturn remove('/userlist',params)
}//添加
export function addUserList(params) {//params 添加的数据return post("/userlist",params);
}//修改
export function updateUserList(id,params) {//id 要修改哪一条数据,params修改的数据return update('/userlist',id,params)
}
4.3.2、mock数据
本次项目中所有数据都是存储在mock数据里,大概的结构可以先了解一下。
4.4、增删改查实现
4.4.1、用户列表 (mock数据)
用户列表页面是对用户的信息进行管理,即增删改查。
页面布局如下:
- 查
实现思路:
1,页面初次加载时,发起请求(不传参),获取所有数据(封装获取所有数据的函数);
2,在搜索框输入用户名,点击搜索(此处双向绑定处理),发请求(传参),响应式渲染;
实现代码:
1、查询数据(不传参)
//获取所有数据的函数
getAllData() {getUserList().then(data=>{data.forEach((item,idx)=>{item.key = idx})this.setState({data:data})})
}//渲染完毕调用函数
componentDidMount(){this.getAllData()
}
2、查询数据(传参)
//查询(点击时按条件查询)
getByParams=()=>{//已经规定传的参数为对象let params = this.state.findParamsgetUserList(params).then(data=>{data.forEach((item,idx)=>{item.key = idx})this.setState({data:data})})
}
3、双向绑定处理
//双向绑定(查询)
changeSearch=(e)=>{this.setState({searchData:{...this.state.searchData,[e.target.name]:e.target.value}})
}
注意:每条数据必须有key,否则会报错!
- 增
实现思路:
1、用户点击增加按钮,弹出添加框;
2、用户输入数据(此处做双向绑定处理);
3、点击保存,发起请求,将数据添加到数据库中;
4、重新获取数据。
添加框:
//双向绑定处理
this.state = {modelData:{"id": '',"tel": '',"username": "","age": '',"sex": "","regist_time": "","ligin_count": '15',"code": '700',"ip_adress": ""}
}//双向绑定(添加弹框里输入框的值)
changeVal=(ev)=>{this.setState({modelData:{...this.state.modelData,[ev.target.name]:ev.target.value}})
}//显示添加框
showAdd=()=>{ this.setState({isshowAdd:'block'})
}//添加数据
addUser=()=>{addUserList(this.state.modelData).then(data=>{this.setState({isshowAdd:"none", modelData:{"id": '',"tel": '',"username": "","age": '',"sex": "","regist_time": "","ligin_count": '15',"code": '700',"ip_adress": "127.0.0.1"}},()=>{this.getAllData();}); });
}
- 改
实现思路:
1,点击修改按钮,弹出修改框,同时将record,(当前行的数据)传递;
2,修改框中显示当前行数据,用户开始修改数据(此处双向绑定处理);
3,点击修改,发起请求,修改数据;
4,重新获取数据。
//显示修改框
updateData=(idx)=>{//显示修改框,并更新当前修改的index及双向绑定的数据this.setState({isshowUpdate:'block',index:idx.id,modelData:idx})
}//点击修改,修改数据
upDate=()=>{//修改指定数据let index = this.state.indexconsole.log('index:',index);updateUserList(index,this.state.modelData).then(data=>{this.setState({isshowUpdate:'none',modelData:{"id": "","tel": "","username": "","age": "","sex": "","regist_time": "","ligin_count": 15,"code": 700,"ip_adress": "127.0.0.1"}},()=>{console.log('this.state.modelData',this.state.modelData);this.getAllData()}) })
}
- 删
实现思路:
1,点击删除按钮,弹出确认提示框,同时传递id;
2,点击确认,发起请求,删除数据;
3,重新获取数据。
弹出确认框:
//显示删除确认框
showConfirm =(idx)=>{let that = thisconst { confirm } = Modal; confirm({icon: <ExclamationCircleOutlined />,content: <p>确定删除吗?</p>,onOk(){that.removeData(idx)},onCancel(){that.destroyAll()} })}//删除
removeData=(idx)=>{removeUserList(this.state.data[idx].id).then(data=>{this.getAllData()})
}
4.4.2、通讯信息 (封装axios+antd组件)
通讯信息是针对用户列表的通讯信息进行管理,主要实现是通过对通讯信息的增删改查操作。
实现思路及步骤和用户列表类似,我就不写实现思路了。主要说一下不同的技术点。
- 查
1、不传参(获取所有数据)
//获取全部数据
getAllData(){getUserComm().then(data=>{data.forEach((item,idx)=>{item.key = idx})this.setState({data:data})})
}//模板渲染完成获取数据
componentDidMount(){this.getAllData()
}
2、传参(点击搜索按钮)
findData=()=>{//参数为对象let username = this.state.searchDatagetUserComm(username) .then(data=>{data.forEach((item,idx)=>{item.key = idx})this.setState({data:data})})
}
3、双向绑定处理
//双向绑定(查询)
changeSearch=(e)=>{this.setState({searchData:{...this.state.searchData,[e.target.name]:e.target.value}})}
- 增
显示添加框
实例代码:
//显示添加框
showAddComm=()=>{this.showModal()
}//添加数据
addCommu=()=>{addUserComm(this.state.changeData).then(data=>{this.setState({changeData:{"id":"","username":"","tel":"","states":"待审核","local":"","adress":"","mailcode":"","qqcode":"","email":""},visible: false,},()=>{this.getAllData()});})
}
- 改
显示修改框
实例代码:
//显示修改框
updateData=(idx)=>{this.setState({visibleUpd:true,index:idx.id,changeData:idx})
}//修改数据
updDatas=()=>{let index = this.state.indexupdateUserComm(index,this.state.changeData).then(data=>{this.setState({visibleUpd:false,changeData:{"id":"","username":"","tel":"","states":"待审核","local":"","adress":"","mailcode":"","qqcode":"","email":""}},()=>{this.getAllData()}) })
}
- 删
//删除(ID值)
removeData=(idx)=>{let id = this.state.data[idx].idremoveUserComm(id).then(()=>{this.getAllData()})
}
提示:在react中,事件函数里的this指向为undefined,使用this时注意。
4.4.3、商户列表 (async+await)
async和await是es7新增,用于彻底解决回调地狱的问题。也就是把异步代码变为同步代码。
当然也是有前提的,修饰async的函数必须返回promise对象(本次项目封装axios就是返回promise对象,所以不必担心),有兴趣的可以点击我之前的博客查看
在这个模块里,我主要使用async和await把异步代码改为同步代码,
这样不仅减少了代码量,也大大提高了可读性。
商户列表就是对商家信息管理,依旧是增删改查的形式。(实现思路参考用户列表部分)
- 查
//获取全部数据(async)
async getAllData(){//data相当于.then(data)的形参data,也就是后端返回的数据let data = await getmerchanlist()//当使用了await,只有修饰了await的异步代码执行完毕,后面的代码才能接着执行data.forEach((item,idx)=>{item.key = idx})this.setState({data:data})
}//模板渲染完成获取数据
componentDidMount(){this.getAllData()
}
- 增
//显示添加框
showAddComm=()=>{this.showModal()
}//添加数据
async addCommu(){await addmerchanlist(this.state.changeData)this.setState({changeData:{"id":"","merchanname":"","type":"","cost":"","commrating":"","local":"","num":"","tranroute":"","tel":"","username":""},visible: false,},()=>{this.getAllData()});
}
- 改
//显示修改框
updateData=(idx)=>{this.setState({visibleUpd:true,index:idx.id,changeData:idx})
}//修改数据
async updDatas(){let index = this.state.indexawait updatemerchanlist(index,this.state.changeData)this.setState({visibleUpd:false,changeData:{"id":"","merchanname":"","type":"","cost":"","commrating":"","local":"","num":"","tranroute":"","tel":"","username":""}},()=>{this.getAllData()})
}
- 删
// 显示确认框
showConfirm =(idx)=>{let that = thisconst { confirm } = Modal; confirm({icon: <ExclamationCircleOutlined />,content: <p>确定删除吗?</p>,onOk(){console.log('onok函数里的this',this);that.removeData(idx)},onCancel(){that.destroyAll()} })
}//删除数据
async removeData(idx){let id = this.state.data[idx].idawait removemerchanlist(id)this.getAllData()
}
4.4.4、登录账户 (hooks)
重点来了!!!
登录账户是对商户登录的账户进行管理,比如账户的安全等级。
我们都知道,在react里只有类组件(有状态组件)才能够操作state,而函数组件(无状态组件)无法操作state。
而hoos可以解决函数组件的这个弊端,在登录账户这个模块我使用hooks,也就是函数组件实现增删改查
。
使用函数组件之前我们需要给函数组件初始状态,相当于类组件里的state。
import {useState,useEffect} from 'react'//loginaccdata为原始数据,初始为空数组,setloginaccdata为更新初始值状态函数
let [loginaccdata,setloginaccdata] = useState([])//显示修改框和添加框
let [showK,changeK] = useState({visible:false,visibleUp:false,disabled:true,bounds: { left: 0, top: 0, bottom: 0, right: 0 },
})//解构
let {visible,visibleUp,disabled,bounds} = showK
// console.log('bounds',bounds);//记录当前修改的数据下标,useState返回的是数组
let [index,changeIndex] = useState(0)//修改框,添加框双向绑定的数据
let [changeData,setChangedata] = useState({"id": "","name": "","accouname": "","tel": "","email": "","registime": "","root": "管理员","roottype": "所有权限","state": "","username": "","workcode": "","department": "","position": "","logincount": "","order": "","safelevel": ""
})//搜索框双向绑定的数据
let [queryObj,changeQue] = useState({'id':""
})
- 查
1、无参(页面初次加载,获取所有数据)
//获取全部数据
async function getAllData() {let data = await getLoginaccou()data.forEach((item,idx)=>{item.key = idx})setloginaccdata(data)
}//带两个参数的,首次渲染完毕才执行该函数,[]指不依赖任何参数
useEffect(()=>{getAllData()
},[])
useEffect()第二个形参为[]时,相当于componentDidMount(),即页面首次渲染完毕执行
2、有参(点击搜索按钮执行)
//搜索数据(条件搜索 id)
async function searchData(){let data = await getLoginaccou(queryObj)data.forEach((item,idx)=>{item.key = idx})setloginaccdata(data)
}
3、搜索条件双向绑定
//查询条件的双向绑定函数
function changeValQ(e) {changeQue({...queryObj,[e.target.name]:e.target.value})
}
- 增
//显示添加框
function showAdd() {showModal()
}//添加数据
async function addCommu(){await addLoginaccou(changeData)//清空双向绑定的数据setChangedata({"id": "","name": "","accouname": "","tel": "","email": "","registime": "","root": "管理员","roottype": "所有权限","state": "","username": "","workcode": "","department": "","position": "","logincount": "","order": "","safelevel": ""
}) handleCancel()getAllData()
}
- 改
//显示修改框
function updateData(index){showModalUp()setChangedata(loginaccdata[index])changeIndex(index)
}//修改数据
async function update() {let id = loginaccdata[index].idawait updateLoginaccou(id,changeData)//清空双向绑定的数据setChangedata({"id": "","name": "","accouname": "","tel": "","email": "","registime": "","root": "管理员","roottype": "所有权限","state": "","username": "","workcode": "","department": "","position": "","logincount": "","order": "","safelevel": ""
})handleCancelUp()getAllData()
}
- 删
//显示删除确认框
function showConfirm(id){const { confirm } = Modal; confirm({icon: <ExclamationCircleOutlined />,content: <p>确定删除吗?</p>,onOk(){removeData(id)},onCancel(){destroyAll()} })
}//删除数据
async function removeData(idx){//idx就是要删除的数据idawait removeLoginaccou(idx)getAllData()
}
函数组件和类组件实现思路都一样,不过函数组件中需要自己定义初始状态,以及修改初始状态的函数,相当于类组件里的setState()函数,只有这样组件才会显示最新的值。
4.4.5、订单列表 (reduX)
还是重点!!!
hooks和reduX应该是本次项目的难点,也是重点,reduX相当于vue中的vueX,也是用来状态管理的。但是个人感觉,reduX比vueX更复杂一些。如果你还不了解reduX,那么可以点击这里reduX。
在前面剖析项目结构时,有一个处理reduX的文件夹,现在我们可以对这个文件夹进行分析了,只有了解了这个,你才能使用reduX。
首先,文件结构是这样的:
index.js配置reduX以及实例化一个store对象,并导出;
alldata.js是拆分的reducer;(是订单列表模块用到的数据)
actions.js提取actions(对数据的操作,包括异步代码)
在index.js中,
import { createStore ,combineReducers,applyMiddleware} from "redux";
import thunk from 'redux-thunk'
import alldata from './alldata'
import isload from './load'let storeReducer = combineReducers({alldata,isload
})let store = createStore(storeReducer,applyMiddleware(thunk))export default store;
在alldata.js中,
var data = []export default (db=data,action)=>{const {type,payload} = actionswitch(type){case "CHANGEDATA":payload.forEach((item,idx)=>item.key = idx)return payload;case "DELDATA":payload.forEach((item,idx)=>item.key = idx)return payload;case "ADDORDER":payload.forEach((item,idx)=>item.key = idx)return payload;case "UPDATT":payload.forEach((item,idx)=>item.key = idx)console.log('修改后传来的payload',payload);return payload;}return db;
}
在actions.js中,
import { getOrderlist ,removeOrderlist,addOrderlist,updateOrderlist} from "../api/orderlist";//查询
export const All = (seardata)=>(async (dispatch)=>{//外层函数的返回值是一个函数let data = await getOrderlist(seardata)dispatch({type:"CHANGEDATA",payload:data})
})//删除
export const DEl = (id) =>(async (dispatch)=>{console.log('DEL执行了');await removeOrderlist(id)let data = await getOrderlist()// console.log('删除后的data',data);dispatch({type:"DELDATA",payload:data})
})//添加
export const ADD = (datainfo)=>(async (dispatch)=>{console.log('保存时传来的datainfo',datainfo);await addOrderlist(datainfo)let data = await getOrderlist()dispatch({type:"ADDORDER",payload:data})})//修改
export const UPD = (setparams) =>(async (dispatch)=>{// console.log('修改派发来的参数',setparams);await updateOrderlist(setparams.id,setparams.data)let data = await getOrderlist()dispatch({type:"UPDATT",payload:data})
})//load的显示与隐藏
export const SHOWLOAD = (payload)=>({type:"SHOWLOAD",payload
})
可能没掌握reduX的人看了有点懵,没关系,其实思路和vueX差不多,区别就是
1)、vue是响应式的,state里的数据改变了,组件中会响应式改变。
2)、而reduX需要订阅数据的变化,然后手动修改数据,才能够响应到页面。
和之前模块不同的是,订单列表模块不仅使用了reduX,还使用了三级路由。
三级路由思路:
1,订单列表页面上方有搜索,添加按钮,以及一个搜索框;
2,下方的内容为三级路由,即动态切换的部分;
3,点击添加,跳转到添加订单页面,点击搜索跳到订单列表页,默认显示订单列表页;
实现代码:
export default function Orderlist(props){return (<div className={style.orderlist}><div className={style.ordertop}><h3>订单列表</h3><Input value={seardata.id} onChange={changeval} size="large" name="id" allowClear="true" style={{width:"300px",marginLeft:"20px"}} placeholder="请输入ID" /><button className="sear" onClick={()=>searchData()}>搜索</button><button onClick={()=>toAdd()}>增加</button> </div><div className={style.ordercont}><Redirect exact={true} from="/layouts/orderlist" to="/layouts/orderlist/orders"></Redirect><Route path="/layouts/orderlist/orders" component={Orders}></Route><Route path="/layouts/orderlist/orderadd" component={Orderadd}></Route></div></div>)
}
页面展示:
- 查
首先定义函数组件的初始状态,以及订阅数据的变化。
import store from '../../store'
import {All,DEl,UPD} from '../../store/actions'//原数据
let [listdata,setlistdata] = useState(store.getState().data)//改变了state,还需要订阅,使得响应式渲染页面
store.subscribe(()=>{setlistdata(store.getState().alldata)
})//首次加载执行
useEffect(()=>{//首次渲染完毕即执行,[]为不依赖任何参数getAllData()
},[])
1、不传参
//获取所有数据
function getAllData(){//派发action,获取所有数据store.dispatch(All())
}
2、传参
//查询条件双向绑定
function changeval(e){setsear({...seardata,[e.target.name]:e.target.value})
}//查询
function searchData(){store.dispatch(All(seardata))
}
- 增
//确认添加函数
function addorder(){store.dispatch(ADD(orderadd))setorderadd({"id": "","ordercode": "","ordertime": "","ordermoney": "","goodsname": "","goodscount": "","ordertype": "","username": "","state": ""})
}
- 改
//显示修改框
function updateData(record){showModal()setorder(record)setindex(record.id)
}//修改数据
function updataorder(){store.dispatch(UPD({id:index,data:orderset}))handleCancel()setorder({"id": "","ordercode": "","ordertime": "","ordermoney": "","goodsname": "","goodscount": "","ordertype": "","username": "","state": ""})
}
- 删
//显示删除确认框
function showConfirm(record){const { confirm } = Modal; confirm({icon: <ExclamationCircleOutlined />,content: <p>确定删除吗?</p>,onOk(){removeData(record.id)},onCancel(){destroyAll()} })
}//删除数据
function removeData(id){store.dispatch(DEl(id))
}
reduX的实现思路就是:
1,组件里派发,可传参,参数为一个对象;
2,actions里发起请求,把需要的值发给reducer;
3,reducer里修改state即定义的数据;
4,组件里用到该数据的地方订阅,响应式渲染。
如果还是不理解的话,下方评论区见!
好了,以上就是本次项目五个模块的业务功能实现,使用到的技术几乎涵盖了react。当然,很多地方还不够完善,可以在评论区指出。
4.5、登录功能实现
后台管理系统中一般是没有注册的,账户都是管理员分配的,并且权限级别不同(这个我们后面再说)。
所以,只需要实现登录功能。(不登录无法进入系统)静态效果前面已经截过图了,可以参考。
由于项目是通过mock数据实现的,所以只能实现简单的账户+密码的形式登录校验。
登录模块实现思路:
1,mock数据添加一个数据,用来记录用户名和密码;
2,封装axios,当前端点击登录发起请求,校验用户名密码是否正确;
3,若账户密码都正确,返回token信息;
4,前端收到token,提示登录成功,在本地保存token以及用户名,同时进入管理系统;
5,若登录失败,提示失败信息。
token是用来验证用户是否合法,只有合法的用户,才能够发请求获取数据。
mock数据展示:
"loginadmin": [{"id": "0001","username": "xx","userpass": "xxx",}
]
封装axios:
//登录(get)
export function getlogin(url,params){let queryStr = queryStrFn(params)return new Promise(function(resolve,reject){service.get(url+"?"+queryStr).then(res=>{if(res.data.length!==0){//查到数据resolve({code:1,token:ranToken(),paths:res.data[0].paths,curd:res.data[0].zsgc});}else{//未查到数据resolve(res.data)}});});
}//随机一个模拟token
function ranToken(){let str = "ZXCVBNMASDFGHJKSRDTJFGLQWERTYUIOP";let str_len = str.length;let token_str = "";//随机一个100位的字符串for(let i=0;i<100;i++){let rannum = parseInt(Math.random()*str_len)token_str+=str.charAt(rannum)}return token_str
}
前端点击登录:
//登录
async tologin(){let data = await loginAdmin(this.state.loginobj)if(data.length!==0){//查到数据,存储tokenmessage.info('登录成功');localStorage.setItem('token',data.token)localStorage.setItem('paths',JSON.stringify(data.paths))localStorage.setItem('username',this.state.loginobj.username)this.props.history.push('/layouts')}else{//未查到数据message.info('登录失败');}
}
在登录页,可以通过手机号登录,也可以通过邮箱登录,这两个组件的切换也是通过二级路由实现的。
登录页二级路由实现代码:
render(){return (<div className={style.login}><div className={style.logincont}><div className={style.loginhead}><div onClick={this.tellogin}>手机号登录</div><div onClick={this.emaillogin}>邮箱登录</div></div><div className={style.logincontt}><Switch><Redirect exact={true} from="/login" to="/login/tellogin"></Redirect><Route path="/login/tellogin" component={Tellogin}></Route><Route path="/login/emaillogin" component={Emaillogin}></Route></Switch></div> </div></div>)
}
react里没有路由守卫,所以需要自己去写一个验证是否登录的功能,在layouts.js中;
//验证是否登录,若未登录跳回登录页
function isLogin(){let username = localStorage.getItem('username')console.log('username',);if(!username){//若未登录,跳到登录页message.info('请先登录吧!');props.history.push('/login')}
}//高阶组件,获取路由上下文
export default withRouter(Layouts)
这里注意了,如果layouts不是通过路由跳转来的页面,就没有路由上下文
,需要用到高阶组件。
4.6、权限处理
至于什么是权限,请参考我的这篇博客,里面详细介绍了权限的概念以及基本的操作思路。
思路在这里就不写了,直接上代码:
mock数据:
"loginadmin": [{"id": "0001","username": "xxx","userpass": "xxx","paths": [{"path": "/layouts/userlist"},{"path": "/layouts/commuinfo"},{"path": "/layouts/userstati"}]}
]
前端登录,并保存path:
//登录
async tologin(){let data = await loginAdmin(this.state.loginobj)// console.log('返回的data',data); if(data.length!==0){//查到数据,存储tokenmessage.info('登录成功');localStorage.setItem('token',data.token)localStorage.setItem('paths',JSON.stringify(data.paths))localStorage.setItem('curd',data.curd)localStorage.setItem('username',this.state.loginobj.username)this.props.history.push('/layouts')}else{//未查到数据message.info('登录失败');}
}
grants筛选用户path:
import Userlist from '../pages/Userlist/userlist'
import Commuinfo from '../pages/commuinfo/commuinfo'
import Userstati from '../pages/userstati/userstati'
import Merchanlist from '../pages/merchanlist/merchanlist'
import Loginaccou from '../pages/loginaccou/loginaccou'
import Orderlist from '../pages/orderlist/orderlist'
import Merchans from '../pages/merchans/merchans'
import Orderstati from '../pages/orderstati/orderstati'let stores = [{ title:"用户列表",path: "/layouts/userlist",component:Userlist},{title:"通讯信息",path: "/layouts/commuinfo",component:Commuinfo},{title:"用户分析",path: "/layouts/userstati",component:Userstati},{title:"商户列表",path: "/layouts/merchanlist",component:Merchanlist},{title:"登录账户",path: "/layouts/loginaccou",component:Loginaccou},{title:"商户分析",path: "/layouts/merchas",component:Merchans},{title:"订单列表",path: "/layouts/orderlist",component:Orderlist},{title:"订单分析",path: "/layouts/orderstati",component:Orderstati}
]//1、获取本地存储的权限path//2、从stores(所有路由配置中筛选出当前登录用户的权限path)
export default () =>{let paths = JSON.parse(localStorage.getItem('paths'))let curd = localStorage.getItem('curd')let userpath = stores.filter(items=>(paths.some((item)=>(item.path == items.path))))userpath[curd] = curd;return userpath;
}
动态显示导航栏:
import React from 'react'
import { Menu } from 'antd';
import { MailOutlined, AppstoreOutlined, SettingOutlined,LayoutOutlined, ShareAltOutlined ,RestOutlined} from '@ant-design/icons';
import {NavLink} from 'react-router-dom'
import userpathsFn from '../../grants'import './aside.css'const { SubMenu } = Menu;export default class Aside extends React.Component {constructor(props){super()this.state = {userpaths:[]}}//获取筛选过后新的用户权限路由配置componentDidMount(){this.setState({userpaths:userpathsFn()})}isshowmeau(meaupath){console.log('userpathsFn()',userpathsFn());let res = userpathsFn().some((item)=>item.path==meaupath)return res}render() {return (<> <Menustyle={{ width: 161 }}defaultOpenKeys={['sub1']}// defaultSelectedKeys={['1']}mode="inline"theme = 'light'><SubMenu key="sub1" icon={<MailOutlined />} title="用户管理">{/* {this.state.userpaths.map((item,idx)=>(<Menu.Item key={idx}><NavLink to={item.path} activeClassName="tochange">{item.title}</NavLink></Menu.Item>))} */}<Menu.Item key="1" style={{display:this.isshowmeau('/layouts/userlist')?"block":"none"}}><NavLink to="/layouts/userlist" activeClassName="tochange">用户列表</NavLink></Menu.Item><Menu.Item key="2" style={{display:this.isshowmeau('/layouts/commuinfo')?"block":"none"}}><NavLink to="/layouts/commuinfo" activeClassName="tochange">通讯信息</NavLink></Menu.Item><Menu.Item key="3" style={{display:this.isshowmeau('/layouts/userstati')?"block":"none"}}><NavLink to="/layouts/userstati" activeClassName="tochange">统计分析</NavLink></Menu.Item></SubMenu><SubMenu key="sub2" icon={<AppstoreOutlined />} title="商户管理"><Menu.Item key="4" style={{display:this.isshowmeau('/layouts/merchanlist')?"block":"none"}}><NavLink to="/layouts/merchanlist" activeClassName="tochange">商户列表</NavLink></Menu.Item><Menu.Item key="5" style={{display:this.isshowmeau('/layouts/loginaccou')?"block":"none"}}><NavLink to="/layouts/loginaccou" activeClassName="tochange">登录账户</NavLink></Menu.Item><Menu.Item key="6" style={{display:"none"}}>登录记录</Menu.Item><Menu.Item key="7" style={{display:"none"}}>资质信息</Menu.Item><Menu.Item key="8" style={{display:this.isshowmeau('/layouts/merchas')?"block":"none"}}><NavLink to="/layouts/merchas" activeClassName="tochange">商户分析</NavLink></Menu.Item></SubMenu> <SubMenu key="sub3" icon={<LayoutOutlined />} title="订单管理"><Menu.Item key="9" style={{display:this.isshowmeau('/layouts/orderlist')?"block":"none"}}><NavLink to="/layouts/orderlist" activeClassName="tochange">订单列表</NavLink></Menu.Item><Menu.Item key="10" style={{display:this.isshowmeau('/layouts/orderstati')?"block":"none"}}><NavLink to="/layouts/orderstati" activeClassName="tochange">统计分析</NavLink></Menu.Item></SubMenu><SubMenu key="sub4" icon={<SettingOutlined />} title="收支管理"><Menu.Item key="11" style={{display:"none"}}>收支列表</Menu.Item><Menu.Item key="12" style={{display:"none"}}>收支分析</Menu.Item></SubMenu><SubMenu key="sub5" icon={<RestOutlined />} title="平台管理"><Menu.Item key="13" style={{display:"none"}}>管理中心</Menu.Item><Menu.Item key="14" style={{display:"none"}}>短信管理</Menu.Item><Menu.Item key="15" style={{display:"none"}}>促销信息</Menu.Item><Menu.Item key="16" style={{display:"none"}}>基本设置</Menu.Item></SubMenu><SubMenu key="sub6" icon={<ShareAltOutlined />} title="安全管理"><Menu.Item key="17" style={{display:"none"}}>基本信息</Menu.Item><Menu.Item key="18" style={{display:"none"}}>密码设置</Menu.Item></SubMenu></Menu></>);}
}
4.7、echarts数据可视化处理
echarts是一款基于js的数据可视化第三方库,在这次项目也使用到,对数据统计分析一目了然。特别是在后台管理系统,使用的非常频繁。
在这次项目里,主要针对用户管理模块和商户管理模块的数据进行了简单的可视化分析。
4.7.1、用户管理模块(注册用户性别占比分析)
import * as echarts from 'echarts';
import React from 'react'
import {getUserList} from '../../api/userlist'export default class Listtel extends React.Component{constructor(){super()this.state ={ boy:0,girl:0 }}getAllData(){let boy = 0;let girl = 0;getUserList().then(data=>{//异步的console.log("echarts里的data:",data);data.forEach((item,idx)=>{if(item.sex==="男"){boy ++;}else{girl ++;}})this.setState({boy:boy,girl:girl},()=>{// 基于准备好的dom,初始化echarts实例var myChart = echarts.init(document.getElementById('listmain'));console.log('统计后的boy22',this.state.boy);// 绘制图表myChart.setOption({title: {text: '注册用户年龄分析',left:"center",top:8},xAxis: {type: 'category',splitNumber:10,name:"年龄分布",data: ['0-10', '10-20', '20-30', '30-40', '40-50', '50-60', '60-70','70-80','80-90','90-100'],axisTick: {length: 10,interval:0,show:true,lineStyle: {type: 'dashed'// ...}}},yAxis: {type: 'value',name:"人数",axisTick: {length: 10,lineStyle: {type: 'dashed'// ...}}},series: [{data: [120, 200, 150, 80, 70, 110, 130,45,87,90],type: 'bar',showBackground: true,backgroundStyle: {color: 'rgba(180, 180, 180, 0.2)'}}]});})})}componentDidMount(){//获取数据this.getAllData()}render(){return (<div id="listmain" style={{width:"600px",height:"500px"}}></div>)}
}
4.7.2、用户管理模块(注册用户年龄分析)
import * as echarts from 'echarts';
import React from 'react'
import {getUserList} from '../../api/userlist'export default class Listtel extends React.Component{constructor(){super()this.state ={ boy:0,girl:0 }}getAllData(){let boy = 0;let girl = 0;getUserList().then(data=>{//异步的console.log("echarts里的data:",data);data.forEach((item,idx)=>{if(item.sex==="男"){boy ++;}else{girl ++;}})this.setState({boy:boy,girl:girl},()=>{// 基于准备好的dom,初始化echarts实例var myChart = echarts.init(document.getElementById('main'));console.log('统计后的boy22',this.state.boy);// 绘制图表myChart.setOption({title: {text: '注册用户性别占比分析图',left:"center",top:8},tooltip: {trigger: 'item'},legend: {top: '8%',left: 'center'},labelLine: {show: true},series: [{name: '访问来源',type: 'pie',radius: ['40%', '70%'],avoidLabelOverlap: false,itemStyle: {borderRadius: 10,borderColor: '#fff',borderWidth: 2},label: {show: false,position: 'center'},emphasis: {label: {show: true,fontSize: '40',fontWeight: 'bold'}},labelLine: {show: false},data: [{value: this.state.boy, name: '男生'},{value: this.state.girl, name: '女生'},]}]});})})}componentDidMount(){//获取数据this.getAllData()}render(){return (<div id="main" style={{width:"500px",height:"500px"}}></div>)}
}
效果图:
4.7.3、商户管理模块(商户经营餐饮分析)
import * as echarts from 'echarts';
import React from 'react'
import { getmerchanlist } from '../../api/merchanlist'var legendData = [];
var seriesData = [];export default class Merchanstati extends React.Component {constructor() {super()}//获取所有数据async getAllData() {let data = await getmerchanlist()data.forEach((item) => {legendData.push(item.merchanname)seriesData.push({name: item.merchanname,value: item.cost})})// 基于准备好的dom,初始化echarts实例var myChart = echarts.init(document.getElementById('merchanstati'));myChart.setOption({title: {text: '商户经营餐饮分析以及人均消费占比',left: 'left'},tooltip: {trigger: 'item',formatter: '{a} <br/>{b} : {c} ({d}%)',},legend: {type: 'scroll',orient: 'vertical',right: 120,top: 20,bottom: 20,data: legendData,// selected: legendData.selected},series: [{name: '姓名',type: 'pie',radius: '55%',center: ['40%', '50%'],data: seriesData,emphasis: {itemStyle: {shadowBlur: 10,shadowOffsetX: 0,shadowColor: 'rgba(0, 0, 0, 0.5)'}}}]})}componentDidMount(){legendData = [];seriesData = [];this.getAllData()}render() {return (<div id="merchanstati" style={{ width: "100%", height: "100%"}}></div>)}
}
4.7.4、商户管理模块(商户登录分析)
import * as echarts from 'echarts';
import React from 'react'
import { getLoginaccou } from '../../api/loginaccou'var legendData = [];//记录部门分类export default class Merchanstati extends React.Component {constructor() {super()}//数组去重getOnlyArr(arr) {let newarr = []for (let i = 0; i < arr.length; i++) {if (newarr.indexOf(arr[i]) == -1) {newarr.push(arr[i])}}return newarr;}//获取所有数据async getAllData() {let data = await getLoginaccou()console.log('data',data);data.forEach((item) => {legendData.push(item.department)// seriesData.push({// name: item.merchanname,// value: item.cost// })})legendData= this.getOnlyArr(legendData)console.log('legendData',legendData);// 基于准备好的dom,初始化echarts实例var myChart = echarts.init(document.getElementById('merchanlogin'));myChart.setOption({title: {text: '商户部门登录分析'},tooltip: {trigger: 'axis'},legend: {data: ['部门登录分析']},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},toolbox: {feature: {saveAsImage: {}}},xAxis: {type: 'category',boundaryGap: false,data: legendData},yAxis: {type: 'value'},series: [{name: '部门登录分析',type: 'line',stack: '总量',data: [120, 132, 101, 134, 90, 230, 210]}]})}componentDidMount() {this.getAllData()}render() {return (<div id="merchanlogin" style={{ width: "100%", height: "100%" }}></div>)}
}
效果图:
个别echarts图表效果不全,但是思路是一样的,只是echarts使用还不够熟练。
五、项目总结
1)、这次的项目使用到的第三方组件库基本都是antd,说实话,antd这个库不同于vant,如果是第一次使用,将会遇到特别大的阻力。所以,还需要多使用,增加熟练度;
2)、对于react的使用还不够熟练,尤其是hooks性能优化方面;
3)、功能实现上太过简单,尤其是用mock数据实现的,本来打算用nodeJS写接口的;
源码下载地址:https://gitee.com/sandas/order-admin
运行项目需要登录,可以私信!