准备
建立项目
npm install -g @vue/cli
vue create managesys--创建vue2
配置其他
router
npm install vue-router@3.5.0
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const routers=[]
const router=new Router({ routes:routers})
export default router
store
npm install vuex@3.1.2 --save
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store=new Vuex.Store({
state:{},
mutation:{},
})
export default store
axios
npm install axios@0.18.0 --save
api/config.js
import axios from "axios";
export function request(config) {
const instance = axios.create({
//https://www.fastmock.site/mock/b7da26133fab0e187542cfe12815754f/system
baseURL:
"https://www.fastmock.site/mock/b7da26133fab0e187542cfe12815754f/system",
timeout: 50000,
});
//再次基础上可以添加请求拦截器和响应拦截器
return instance(config);
}
路径别名
//src/vue.config.js 这里的webpack配置会和公共的webpack.config.js进行合并
const path = require('path');
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
lintOnSave: false,//是否再保存的时候使用'eslint-loader进行检查 默认为true 最好修改为false
chainWebpack: config => {
config.resolve.alias
.set('@', resolve('src'))
.set('assets', resolve('src/assets'))
.set('components', resolve('src/components'))
.set('api', resolve('src/api'))
.set('views', resolve('src/views'))
}
}
element UI
npm i element-ui -S
//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import http from '@/api/config'//axios
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false
Vue.prototype.$http=http
Vue.use(ElementUI);
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
组件
框架
elementUI-container容器实现
Main.vue
<template>
<el-container style="height: 100%">
<!-- 左侧栏 -->
<el-aside width="auto">
<!-- 左侧栏控件 -->
<common-aside></common-aside>
</el-aside>
<!-- 右侧栏 -->
<el-container>
<!-- header部分 -->
<el-header> 23 </el-header>
<!-- 内容区域 -->
<el-main> er </el-main>
</el-container>
</el-container>
</template>
<script>
import commonAside from "components/CommonAside";
export default {
name: "mainIndex",
components: { commonAside },
};
侧边导航栏
Menu组件
循环遍历路由,或者data数据,设置menu-item
通过name属性进行路由的跳转是最便利的
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
左侧栏和header部分对于整个后台部分都是不变的,因此里面显示的vue组件,都是Main.vue的子组件
path:'/',
component:()=>import('views/Mainindex'),
children:[
{
path:'/',
name:'home',
component:()=>import('views/Home/Home')
},
{
path:'/mall',
name:'mall',
component:()=>import('views/MallManage/MallManage')
},
{
path:'/other',
component:()=>import('views/Other/index'),
children:[
{
path:'/page1',
name: "page1",
component:()=>import('views/Other/PageOne')
},
{
path:'/page2',
name: "page2",
component:()=>import('views/Other/PageTwo')
}
]
}
]
},
头部栏
侧边导航栏的显示与折叠 以及个人中心
将iscollapse这个变量存储在store中
<div style="color: aliceblue;height:60px;line-height:60px;">
<div class="l-content" style=" float: left">
<el-button icon="el-icon-menu" @click="collapseMenu"></el-button>
首页
</div>
<div class="r-content" style=" float: right;">
<el-dropdown trigger="click">
<span class="el-dropdown-link">
用户
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
export default {
name: "commonHeader",
methods:{
collapseMenu(){
this.$store.commit('collapseMenu')
}
}
};
const store=new Vuex.Store({
state:{
iscollapse:false//false表示侧边导航栏展开
},
mutations:{
collapseMenu(state){
state.iscollapse=!state.iscollapse
}
},
})
首页
结合栅格row和col进行划分
再分区进行内容填充
<template>
<el-row :gutter="20" v-if="this.tableData.length">
<el-col :span="8">
<el-card class="box-card" style="margin-top: 10px">
<div slot="header" class="clearfix">
<div class="user">
<img src="" alt="" />
<div class="userinfo">
<p class="name">Admin</p>
<p class="acess">超级管理员</p>
</div>
</div>
</div>
<div class="login-info">
<p>上次登录的时间:</p>
<p>上次登录的地点:</p>
</div>
</el-card>
<el-card class="box-card" style="margin-top: 10px; ">
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="name" label="课程"> </el-table-column>
<el-table-column prop="todayBuy" label="今日购买"> </el-table-column>
<el-table-column prop="monthBuy" label="本月购买"> </el-table-column>
<el-table-column prop="totalBuy" label="总购买"> </el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="16">
<div class="num">
<el-card
v-for="(item, index) in countData"
:key="item.name"
class="num-item"
:body-style="{ display: 'flex', padding: 0 }"
>
<div class="item-icon" :style="'backgroundColor:' + item.color">
<i :class="'el-icon-' + item.icon"></i>
</div>
<div class="item-text">
<p style="font-size:25px">¥ {{ item.value }}</p>
<p>{{ item.name }}</p>
</div>
</el-card>
</div>
<el-card> </el-card>
<el-row>
<el-col :span="12">
<el-card></el-card>
</el-col>
<el-col :span="12">
<el-card></el-card>
</el-col>
</el-row>
</el-col>
</el-row>
</template>
<script>
import { request } from "api/config";
export default {
name: "homeView",
data() {
return {
tableData: [],
countData: [
{
name: "今日支付订单",
value: 1234,
icon: "success",
color: "#2ec7c9",
},
{
name: "今日收藏订单",
value: 210,
icon: "star-on",
color: "#ffb980",
},
{
name: "今日未支付订单",
value: 1234,
icon: "s-goods",
color: "#5ab1ef",
},
{
name: "本月支付订单",
value: 1234,
icon: "success",
color: "#2ec7c9",
},
{
name: "本月收藏订单",
value: 210,
icon: "star-on",
color: "#ffb980",
},
{
name: "本月未支付订单",
value: 1234,
icon: "s-goods",
color: "#5ab1ef",
},
],
};
},
mounted() {
this.getTableData();
},
methods: {
getTableData() {
request({
url: "/home/getData",
methods: "get",
}).then((res) => {
console.log(res.data.data.tableData);
this.tableData = res.data.data.tableData;
});
},
},
};
</script>
<style scoped>
.num {
display: flex;
flex-wrap: wrap;
align-content: center;
justify-content: space-around;
}
.num-item {
width: 290px;
margin-top: 20px;
}
.item-icon {
width: 40%;
line-height: 116.9px;
}
.item-text{
text-align: center;
flex: 1;
}
面包屑+tab切换功能
利用Element-UI中的面包屑和tag标签
面包屑
面包屑导航的难度在于如何将路由与面包屑导航进行映射
首先面包屑首页一定要存在,接下来,侧边组建点击某个菜单,把这个数据存在vuex中,然后头部组件来获取vuex中这个数据并展示
都是利用$route的match属性来实现面包屑功能,此时需要给路由映射表中,添加meta属性,以及name属性
首先是能够实现渲染,二是点击能够跳转到相应的路由
router/index.js
path: "/mall",
name: "mall",
meta: {
title: "商品管理",
},
component: () => import("views/MallManage/MallManage"),
},
{
path: "/other",
component: () => import("views/Other/index"),
children: [
{
path: "/page1",
name: "page1",
meta: {
title: "页面1",
},
CommonAside
mounted() {
//初始路由,存储首页
let matched = this.$route.matched.filter((item) => item.name);
this.$store.commit("selectMenu", matched[0]);
},
methods: {
// 点击后实现路由的跳转
clickMenu(item) {
this.$router.push({ name: item.name });
},
},
watch: {
$route: {
//监听路由的修改改变面包屑
handler() {
let matched = this.$route.matched.filter((item) => item.name);
//以及修改面包屑
this.$store.commit("selectMenu", matched[0]);
},
},
},
再去修改vuex中的变量,结合最终显示的效果进行数组的变化
state:{
userInfo:{},
iscollapse:false,//false表示侧边导航栏展开
currentMenu:[{path:'/',title:'首页'}],
tabsList:[{path:'/',name:'home',label:'首页',icon:'home'}]
},
mutations:{
// 侧边栏是否展开
collapseMenu(state){
state.iscollapse=!state.iscollapse
},
//获取用户信息
getuserInfo(state,payload){
state.userInfo=payload
},
//选择标签,选择面包屑
selectMenu(state,payload){
let obj={path:payload.path,title:payload.meta.title}
// if(payload.name!='home'&& findItem(state.currentMenu,'title',obj.title)==-1){
// state.currentMenu.push(obj)
// }
if(payload.name!='home'){
if(state.currentMenu.length==2){
state.currentMenu.pop()
}
state.currentMenu.push(obj)
}
if(payload.name=='home'&&state.currentMenu.length==2){
state.currentMenu.pop()
}
},
},
渲染到表头
CommonHeader
<!-- 面包屑 -->
<el-breadcrumb separator-class="el-icon-arrow-right" style="margin-left:20px;">
<el-breadcrumb-item v-for="(item,index) in $store.state.currentMenu" :to="item.path" :key=index>{{ item.title }}</el-breadcrumb-item>
</el-breadcrumb>
computed:{
current(){
return this.$store.state.currentMenu
}
$route.matched
比如目前的路由是/a/aa/aaa,那么此时this.$route.matched匹配到的会是一个数组,包含’/‘,‘/a’,’/aa’,'/aaa’这四个路由信息,从而可以直接利用路由信息渲染面包屑导航
判断一个数组对象中是否含有某个对象
let arr=[
{num:1},
{num:2},
{num:3}
]
let obj={num:2}
let obj2=arr[1]
console.log(arr.includes(obj),arr.includes(obj2));//false true
因为里面的引用类型是判断引用地址,obj重新开辟了地址,因此判断为false
???目的是,存在一个obj,要判断出arr是含有obj内容的
1、“JSON.stringify”+‘indexOf’/‘include’
把原来的数组和开辟新地址的对象,转变为字符串,之后通过indexOf判断下标是否为-1
console.log(JSON.stringify(arr).includes(JSON.stringify(obj)))//true
缺点:开销大
以及如果数组或者对象里面含有函数的话,就不能使用
2、通过对象的唯一键值对进行判断
function findItem(arr,key,val) {
for(let i = 0; i < arr.length; i++) {
if(arr[i][key] == val) {
return i
}
}
return -1
}
// if(payload.name!='home'&& findItem(state.currentMenu,'title',obj.title)==-1){
// state.currentMenu.push(obj)
// }
3、使用findIndex
tabsList: [
{
path: '/',
name: 'home',
label: '首页',
icon: 'home'
}
]
selectMenu(state, val) {
if (val.name === 'home') {
state.currentMenu = null
} else {
state.currentMenu = val
//如果等于-1说明tabsList不存在那么插入,否则什么都不做
let result = state.tabsList.findIndex(item => item.name === val.name)
result === -1 ? state.tabsList.push(val) : ''
}
tab切换
1、实现显示,首页tag一开始就会存在,而且是不能进行删除的
2、点击:切换到相应的路由,以及改变背景颜色,当点击左侧栏时,如果tag没有该菜单名称则新增,如果已经有了那么当前tag背景颜色加深
3、可以删除,删除当前tag,如果是最后一个,那么路由调整到他前面那个标签,并且背景变蓝,如果不是最后一个那么路由调整到他后面那个标签对应的路由
主要就是elemntUI中el-tag标签的使用
<div style="margin-left: 10px; text-align: left; margin-top: 20px">
<el-tag
v-for="(tag) in $store.state.tabsList"
:key="tag.title"
:closable="tag.title!=='首页'"//标签可关闭属性
@close="handleClose(tag)"
style="margin-left: 10px"
:effect="$route.meta.title==tag.title?'dark':'plain'"//选中当前路由,tag变色
@click="chosetag(tag)"//点击tag进行路由跳转
>
{{ tag.title }}
</el-tag>
</div>
export default {
data() {
return {
currentindex: 0,
ischose: [false, false, false, false],
};
},
computed: {
tags() {
return this.$store.tabsList;
},
},
methods: {
chosetag(tag) {
// 点击tag时候,切换路由
console.log(tag);
this.$router.push({path:tag.path})
},
handleClose(tag) {//不完整,没有考虑删除tag标签与当前路由一致的情况
this.$store.commit("closetag", tag);
},
},
};
完整:
chosetag(tag) {
// 点击tag时候,切换路由
this.$router.push({ path: tag.path });
},
handleClose(tag, index) {
//删除当前tag,如果是最后一个,那么路由调整到他前面那个标签,并且背景变蓝,如果不是最后一个那么路由调整到他后面那个标签对应的路由
//如果删掉这个标签与路由有关
if (tag.path == this.$route.path) {
//如果删掉的是最后一个那么就让他前移
if (index == this.$store.state.tabsList.length - 1) {
this.$router.push({
path: this.$store.state.tabsList[index - 1].path,
});
} else {
//如果删掉的是中间的tag。那么让路由后移
this.$router.push({
path: this.$store.state.tabsList[index + 1].path,
});
}
}
//如果删掉的这个标签与当前路由无关,那么就直接删除
this.$store.commit("closetag", tag);
},
},
closetag(state,payload){
state.tabsList.splice(state.tabsList.indexOf(payload),1)
}
//选择标签,选择面包屑
selectMenu(state,payload){
let obj={path:payload.path,title:payload.meta.title}
if(payload.name!='home'&& findItem(state.tabsList,'title',obj.title)==-1){//判断之前的数组对象中,是否含有某个对象
state.tabsList.push(obj)
}
currentMenu:[{path:'/',title:'首页'}],
tabsList:[{path:'/',title:'首页'}]
},
Vue.use(Vuex)
function findItem(arr,key,val){
for(let i=0;i<arr.length;i++){
if(arr[i][key]==val){
return i
}
}
return -1
}
封装ECharts组件-折线图、直方图、饼图
场景:订单数量表,商品销量表,会员数量表等,以折线图、柱状图、饼状图等方式呈现
数据传入组件
数据形式:
title:标题;tooltip:提示框组件;legend:图例组件;xAxis:直角坐标轴中的x轴;yAxis:直角坐标轴中的y轴;seried:系列列表,每个系列通过type决定自己的图标类型
对数据进行处理成series需要的格式
echartData: {
order: {
xData: [],
series: [],
},
},
//数据处理
//1、取出series需要的name部分-键名
let keyArray=Object.keys(order.data[0])//['苹果','vivo','oppo','。。。']
//2、循环添加数据
keyArray.forEach(key=>{
this.echartData.order.series.push({
name:key==='wechat'?'小程序':key,
data:order.data.map(item=>item[key]),
type:'line'
})
})
});
echarts.vue
<template>
<div style="height: 100%" ref="echart">rcharts</div>
</template>
<script>
import * as echarts from "echarts";
export default {
props: {
//接受父组件数据:1、chartData(series数据+x轴坐标系数据)
//2、isAxisChart(是否有x坐标系,如果为false,则xData就为空)
chartData: {
type: Object,
default() {
return {
xData: [],
series: [],
};
},
},
isAxisChart: {
type: Boolean,
default: true,
},
},
computed:{
options(){
return this.isAxisChart?this.axisOption:this.normalOption
}
},
data() {
return {
echart: null,
axisOption: {
legend: { textStyle: { color: "#333" } },
grid: { left: "20%" },
tooltip: { trigger: "axis" },
xAxis: {
type: "category",
data: [], //横坐标
axisLine: { lineStyle: { color: "#17b3a3" } },
axisLabel: { color: "#333" },
boundaryGap: false,
},
yAxis: [
{
type: "value",
axisLine: { lineStyle: { color: "#17b3a3", type: "dashed" } },
axisLabel: { color: "#333" },
},
],
color: [
"#2ec7c9",
"#b6a2de",
"#5ab1ef",
"#ffb980",
"#d87a80",
"#8d98b3",
"#e5cf0d",
"#97b552",
"#95706d",
"#dc69aa",
"#07a2a4",
"#9a7fd1",
"#588dd5",
"#f5994e",
"#c05050",
"#59678c",
"#c9ab00",
"#7eb00a",
"#6f5553",
"#c14089",
],
series: [], //要展示的数据
},
normalOption: {
tooltip: { trigger: "item" },
color: [
"#0f78f4",
"#dd536b",
"#9462e5",
"#a6a6a6",
"#e1bb22",
"#39c362",
"#3ed1cf",
],
series: [],
},
};
},
mounted() {
this.initChart();
//resize改变图标尺寸,在容器大小发生改变时需要手动调用,(因为侧边栏是可以收缩的,所以图标根据是否收缩来改变尺寸)
window.addEventListener('resize',this.resizeChart)
},
watch: {
//实际中数据是在不断变化的
chartData: {
handler: function () {
this.initChart();
},
deep: true,
},
},
methods: {
initChart() {
//获取处理好的数据
this.initChartData();
if (this.echart) {
this.echart.setOption(this.options);
} else {
this.echart = echarts.init(this.$refs.echart);
this.echart.setOption(this.options);
}
},
//处理数据
initChartData() {
if (this.isAxisChart) {
this.axisOption.xAxis.data = this.chartData.xData;
this.axisOption.series = this.chartData.series;
} else {
//饼状图不需要横坐标
this.normalOption.series = this.chartData.series;
}
},
resizeChart(){
this.echart?this.echart.resize():""
}
},
destoryed(){
window.removeEventListener('resize',this.resizeChart)
}
};
</script>
<style></style>
在使用这个组件的时候,父组件只要传入必要的数据即可
<el-card style="margin-top: 20px"
><Echarts
:chartData="echartData.order"
style="height: 280px"
v-if="echartData.order.xData.length"
></Echarts>
</el-card>
<el-row>
<el-col :span="12" style="margin-top: 20px">
<el-card>
<Echarts
:chartData="echartData.user"
style="height: 240px"
v-if="echartData.user.xData.length"
></Echarts>
</el-card>
</el-col>
<el-col :span="12" style="margin-top: 20px">
<el-card>
<Echarts
:chartData="echartData.video"
style="height: 240px"
v-if="echartData.video.series.length"
:isAxisChart="false"
></Echarts>
</el-card>
</el-col>
</el-row>
</el-col>
</el-row>
echartData: {
order: {
//折线图
xData: [],
series: [],
},
user: {
//直方图
xData: [],
series: [],
},
//饼状图,不需要横坐标
video: {
series: [],
},
},
};
getTableData() {
request({
url: "/home/getData",
methods: "get",
}).then((res) => {
this.tableData = res.data.data.tableData; //获取表格数据
//订单折线图
const order = res.data.data.orderData;
this.echartData.order.xData = order.date;
//数据处理
//1、取出series需要的name部分-键名
let keyArray = Object.keys(order.data[0]); //['苹果','vivo','oppo','。。。']
//2、循环添加数据
keyArray.forEach((key) => {
this.echartData.order.series.push({
name: key === "wechat" ? "小程序" : key,
data: order.data.map((item) => item[key]),
type: "line",
});
});
//用户柱状图
const user = res.data.data.userData;
this.echartData.user.xData = user.map((item) => item.date);
this.echartData.user.series.push({
name: "新增客户",
data: user.map((item) => item.new),
type: "bar",
});
this.echartData.user.series.push({
name: "活跃客户",
data: user.map((item) => item.active),
type: "bar",
barGap: 0,
});
//饼状图
const video = res.data.data.videoData;
this.echartData.video.series.push({
data: video,
type: "pie",
});
});
},
组件封装思路????
伪造数据,使用mock.js更加方便
npm install mockjs
main.js
import router from './router'
import store from './store'
import './mock'
用户管理
table表格组件的使用
form表单实现对表格数据的增删改查
将从后端得到的数据渲染到表格中
tableLabel: [//表格的label标签,表头
{ prop: "name", label: "姓名" },
{ prop: "age", label: "年龄" },
{
prop: "sex",
label: "性别",
},
{ prop: "birth", label: "出生日期" },
{ prop: "addr", label: "地址" },
],
tableData: [],//表格数据,从后端请求回来的数据
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column label="序号" width="180">
<template slot-scope="scope">
//序号部分是写死的数据,结合element-ui文档
<span style="margin-left: 10px">{{
(config.page - 1) * 20 + scope.$index + 1
}}</span>
</template>
</el-table-column>
<el-table-column
:prop="item.prop"
:label="item.label"
width="180"
v-for="(item, index) in tableLabel"
:key="item.label"
>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)"
>编辑</el-button
>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
同时在使用表格的时候,一般都需要加分页器
config: { page: 1, total: 30, load: false },
<!-- 分页 -->
<div class="block">
<el-pagination
layout="prev, pager, next"
:total="config.total"
:page-size="20"
@current-change="changePage"
>
</el-pagination>
</div>
获取数据:表格数据、分页数据(后端实现分页)
methods: {
getdata(name = "") {//name用来实现对表格数据的筛选
this.config.load = true;
//搜索时,页码需要设置为1,才能正确返回数据格式,因为数据是从第一页开始返回的
name ? (this.config.page = 1) : "";
request1({
url: "/user/getUser",
method: "get",
params: {
page: this.config.page,
name,
},
}).then((res) => {
this.config.total = res.data.count;
const response = res.data;
this.tableData = response.list;
this.tableData = this.tableData.filter((item) => {
//对返回的数据进行修改,不用render函数
item.sex = item.sex == 1 ? "男" : "女";
return item;
});
});
},
实现删除数据
//删除元素
handleDelete(index, row) {
//不是删除获取来的数据,要返回给后端进行删除
this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
let id = row.id; //根据唯一的id号去后端进行删除
request1({
url: "/user/del",
method: "get",
params: { id },
}).then(() => {
this.$message({
type: "success",
message: "删除成功!",
});
this.getdata();
});
})
.catch((error) => {
console.log(error);
this.$message({
type: "info",
message: "已取消删除",
});
});
},
###实现修改操作–结合form表单
<!-- 更新用户、新增用户 form表单-->
<el-dialog
:title="oprateType == 'edit' ? '更新用户' : '新增用户'"
:visible.sync="isshow"
width="30%"
>
<span slot="footer" class="dialog-footer"
><el-form
label-position="right"
label-width="80px"
:model="formLabelAlign"
ref="form"
>
<el-form-item
:label="item.label"
v-for="(item, index) in formLable"
:key="item.label"
>
<el-input
v-model="formLabelAlign[item.model]"
v-if="item.type == 'input'"
:placeholder="'请输入' + item.label"
></el-input>
<el-select
v-if="item.type == 'select'"
v-model="formLabelAlign[item.model]"
>
<el-option
v-for="item in item.opts"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
<el-date-picker
v-if="item.type == 'date'"
placeholder="'选择日期'"
v-model="formLabelAlign[item.model]"
type="date"
value-format="yyy-MM-dd"
></el-date-picker>
</el-form-item>
</el-form>
<el-button @click="isshow = false">取 消</el-button>
<el-button type="primary" @click="confirm">确 定</el-button>
</span>
</el-dialog>
结合表单实现添加操作–使用form
使用form表单实现筛选
<!-- 对表格数据的操作 -->
<div class="manage-header">
<el-button type="primary" @click="addUser" size="small">+新增</el-button>
<el-form :inline="true" :model="searchform" class="demo-form-inline">
<el-form-item>
<el-input
v-model="searchform.keyword"
placeholder="输入关键字"
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="getdata(searchform.keyword)"
size="small"
>搜索</el-button
>
</el-form-item>
</el-form>
</div>
前端+后端。自己mock数据的使用,增删改查都是在数组上进行的,因此mock函数里面,使用数组方法:filter、map等
:进一步:对table表格以及form表单进行封装
<div class="common-table">
<!--stripe 是否为斑马纹 v-loading在请求数据未返回的时间有个加载的图案,提高用户体验-->
<el-table :data="tableData" height="90%" stripe v-loading="config.loading">
<!--第一行为序号 默认写死-->
<el-table-column label="序号" width="85">
<!--slot-scope="scope" 这里取到当前单元格,scope.$index就是索引 默认从0开始这里从1开始-->
<template slot-scope="scope">
<span style="margin-left: 10px">{{ (config.page - 1) * 20 + scope.$index + 1 }}</span>
</template>
</el-table-column>
<!--show-overflow-tooltip 当内容过长被隐藏时显示 tooltip-->
<el-table-column show-overflow-tooltip v-for="item in tableLabel" :key="item.prop" :label="item.label" :width="item.width ? item.width : 125">
<!--其实可以在上面:prop="item.prop"就可以显示表单数据 这里设置插槽的方式话更加灵活 我们可以写样式-->
<template slot-scope="scope">
<span style="margin-left: 10px">{{ scope.row[item.prop] }}</span>
</template>
</el-table-column>
<!--操作-->
<el-table-column label="操作" min-width="180">
<template slot-scope="scope">
<el-button size="min" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="min" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--分页-->
<el-pagination class="pager" layout="prev, pager, next" :total="config.total" :current-page.sync="config.page" @current-change="changePage" :page-size="20"></el-pagination>
</div>
</template>
<script>
// config分页数据,这里面至少包括当前页码 总数量
export default {
props: {
tableData: Array,
tableLabel: Array,
config: Object
},
methods: {
//更新
handleEdit(row) {
this.$emit('edit', row)
},
//删除
handleDelete(row) {
this.$emit('del', row)
},
//分页
changePage(page) {
this.$emit('changePage', page)
}
}
}
<!--是否行内表单-->
<el-form :inline="inline" :model="form" ref="form" label-width="100px">
<!--标签显示名称-->
<el-form-item v-for="item in formLabel" :key="item.model" :label="item.label">
<!--根据type来显示是什么标签-->
<el-input v-model="form[item.model]" :placeholder="'请输入' + item.label" v-if="item.type==='input'"></el-input>
<el-select v-model="form[item.model]" placeholder="请选择" v-if="item.type === 'select'">
<!--如果是select或者checkbox 、Radio就还需要选项信息-->
<el-option v-for="item in item.opts" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<el-switch v-model="form[item.model]" v-if="item.type === 'switch'"></el-switch>
<el-date-picker v-model="form[item.model]" type="date" placeholder="选择日期" v-if="item.type === 'date'" value-format="yyyy-MM-dd"> </el-date-picker>
</el-form-item>
<el-form-item><slot></slot></el-form-item>
</el-form>
</template>
<script>
export default {
//inline 属性可以让表单域变为行内的表单域
//form 表单数据 formLabel 是标签数据
props: {
inline: Boolean,
form: Object,
formLabel: Array
}
}
<div class="manage">
<el-dialog :title="operateType === 'add' ? '新增用户' : '更新用户'" :visible.sync="isShow">
<common-form :formLabel="operateFormLabel" :form="operateForm" ref="form"></common-form>
<div slot="footer" class="dialog-footer">
<el-button @click="isShow = false">取 消</el-button>
<el-button type="primary" @click="confirm">确 定</el-button>
</div>
</el-dialog>
<div class="manage-header">
<el-button type="primary" @click="addUser">+ 新增</el-button>
<common-form inline :formLabel="formLabel" :form="searchFrom">
<el-button type="primary" @click="getList(searchFrom.keyword)">搜索</el-button>
</common-form>
</div>
<!--依次是: 表格数据 表格标签数据 分页数据 列表方法 更新方法 删除方法-->
<common-table :tableData="tableData" :tableLabel="tableLabel" :config="config" @changePage="getList()" @edit="editUser" @del="delUser"></common-table>
</div>
权限管理+登录验证+动态路由
场景:
1、登录权限验证:部分页面没有登录不允许访问
2、角色验证:在登录权限的基础上加上角色验证,比如同一个页面两种角色权限看到展示的内容模块不同
1、不同的用户会根据权限不同,在后台管理系统中渲染出不同的菜单栏
2、用户登录之后,会获取路由菜单和一个token,之后跳转的页面都需要带着token
3、用户退出登录,清除动态路由,清除token,跳转到login页面
4、如果当前没有token,那么跳转到任何页面都应该重定向到login页面
参考:
github
mock数据
模拟后端返回的数据源
一般的后台数据库中,就是分为一个user用户表,一个role权限路由表
const dynamicUser = [
{
name: "管理员",
avatar:
"https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/ccb565eca95535ab2caac9f6129b8b7a~300x300.image",
desc: "管理员 - admin",
username: "admin",
password: "654321",
token: "rtVrM4PhiFK8PNopqWuSjsc1n02oKc3f",
routes: [
{
path: "/",
name: "home",
meta: {
title: "首页",
},
component: () => import("views/Home/Home"),
},
{
path: "/mall",
name: "mall",
meta: {
title: "商品管理",
},
component: () => import("views/MallManage/MallManage"),
},
{
path: "/user",
name: "user",
meta: {
title: "用户管理",
},
component: () => import("views/UserManage/UserManage"),
},
{
path: "/other",
component: () => import("views/Other/index"),
children: [
{
path: "/page1",
name: "page1",
meta: {
title: "页面1",
},
component: () => import("views/Other/PageOne"),
},
{
path: "/page2",
name: "page2",
meta: {
title: "页面2",
},
component: () => import("views/Other/PageTwo"),
},
],
},
],
},
{
name: "普通用户",
avatar:
"https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/6364348965908f03e6a2dd188816e927~300x300.image",
desc: "普通用户 - people",
username: "people",
password: "123456",
token: "4es8eyDwznXrCX3b3439EmTFnIkrBYWh",
routes: [
{
path: "/",
name: "home",
meta: {
title: "首页",
},
component: () => import("views/Home/Home"),
},
{
path: "/mall",
name: "mall",
meta: {
title: "商品管理",
},
component: () => import("views/MallManage/MallManage"),
},
],
},
];
export default {
getdynamicUser: (config) => {
return {
code: 200,
message: "登录成功",
};
},
};
可以看出,一般登录之后,返回的数据里面,包含了一个用户的姓名、头像以及token。
:
处理方法:所有的路由权限等,都交给后端,后端根据前端的账号密码,去获取角色权限,处理路由,返回的就是已经匹配对应角色的路由
const dynamicUser = [
{
name: "管理员",
avatar:
"https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/ccb565eca95535ab2caac9f6129b8b7a~300x300.image",
desc: "管理员 - admin",
username: "admin",
// password: "654321",
token: "rtVrM4PhiFK8PNopqWuSjsc1n02oKc3f",
routes: [
{
path: "/",
name: "home",
meta: {
title: "首页",
},
component: () => import("views/Home/Home"),
},
{
path: "/mall",
name: "mall",
meta: {
title: "商品管理",
},
component: () => import("views/MallManage/MallManage"),
},
{
path: "/user",
name: "user",
meta: {
title: "用户管理",
},
component: () => import("views/UserManage/UserManage"),
},
{
path: "/other",
component: () => import("views/Other/index"),
children: [
{
path: "/page1",
name: "page1",
meta: {
title: "页面1",
},
component: () => import("views/Other/PageOne"),
},
{
path: "/page2",
name: "page2",
meta: {
title: "页面2",
},
component: () => import("views/Other/PageTwo"),
},
],
},
],
},
{
name: "普通用户",
avatar:
"https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/6364348965908f03e6a2dd188816e927~300x300.image",
desc: "普通用户 - people",
username: "people",
// password: "123456",
token: "4es8eyDwznXrCX3b3439EmTFnIkrBYWh",
routes: [
{
path: "/",
name: "home",
meta: {
title: "首页",
},
component: () => import("views/Home/Home"),
},
{
path: "/mall",
name: "mall",
meta: {
title: "商品管理",
},
component: () => import("views/MallManage/MallManage"),
},
],
},
];
export default {
getdynamicUser: (config) => {
const { username, password } = JSON.parse(config.body);
console.log(JSON.parse(config.body));
//先判断用户是否存在
if (username === "admin" || username === "people") {
//判断账号和密码是否对象
if (username === "admin" && password === "654321") {
return {
code: 200,
message: "登录成功",
data: dynamicUser.filter((item) => {
item.username === username;
}),
};
} else if (username === "peopel" && password === "123456") {
return {
code: 200,
message: "登录成功",
data: dynamicUser.filter((item) => {
item.username === username;
}),
};
}
} else {
return { code: 404, data: { message: "用户不存在" } };
}
},
};
password一般是后端不可能带出来的,routes就是管理员和普通用户的差异化动态路由
在写差异化动态路控制不同用户访问不同的页面,还可以用另外一种思路实现:把完整的静态路由都写在前端router中,然后根据router的meta属性,协商对于user的role,登录的时候,在根据后端返回的权限,去过滤对比权限,把该用户级角色所对应的路由处理好,缺点:想要修改就需要重新打包处理,而且不能经过后台动态新增删除
//代码位置:router/index.js
{
path: '',
component: layout, //整体页面的布局(包含左侧菜单跟主内容区域)
children: [{
path: 'main',
component: main,
meta: {
title: '首页', //菜单名称
roles: ['user', 'admin'], //当前菜单哪些角色可以看到
}
}]
}
登录页
表单处理
<template>
<el-form
:model="dynamicValidateForm"
ref="dynamicValidateForm"
label-width="100px"
class="demo-dynamic"
>
<h3>用户登录</h3>
<el-form-item
prop="username"
label="用户名"
:rules="[
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, message: '用户名长度不能小于3', trigger: ['blur'] },
]"
>
<el-input v-model="dynamicValidateForm.username"></el-input>
</el-form-item>
<el-form-item
prop="password"
label="密码"
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
>
<el-input v-model="dynamicValidateForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('dynamicValidateForm')"
>登录</el-button
>
</el-form-item>
</el-form>
</template>
<script>
import { request1 } from "api/config";
export default {
name: "loginView",
data() {
return {
dynamicValidateForm: {
password: "",
username: "",
},
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
console.log(this.dynamicValidateForm);
if (valid) {
request1({
url: "/permission/getUser",
method: "post",
data: this.dynamicValidateForm,
}).then((res) => {
if (res.data.code == 404) {
this.$message(res.data.data.message);
}
if (res.data.code == 200) {
this.$router.replace({path:'/main',componnet:()=>require('views/Mainindex')})
}
});
} else {
console.log("error submit!!");
return false;
}
});
},
},
验证表单后的路由跳转问题
动态路由addRoute()
vue2中可以通过路由的addRoutes()和addRoute()两种方法实现路由动态渲染
但是在Vue3中,付日期了addRoutes()方法,只保留了addRoute()单个添加路由配置的方法
router.addRoute()接受的是一个路由规则,也就是一个对象,或者接受一个字符串和一个对象
使用场景:
假设登录的使用是普通用户,我们展示默认页面,普通用户不可以去访问管理员的页面,管理员可以访问所有页面
当从后端根据不同用户返回可以访问的路由时,需要将他们添加到动态路由中
routes: [
{
path: "/",
name: "home",
icon: "s-home",
meta: {
title: "首页",
},
component: () => import("views/Home/Home"),
},
{
addMenu(state, router) {
//添加动态路由 主路由为Main.vue
let currentMenu = {
path: "/",
component: () => import(`@/views/Mainindex`),
children: null,
};
//将根据登录用户返回的路由添加到main的子路由中
// currentMenu.children.push(...state.currentMenu);//...state.currentMenu 会造成里面import导入的component属性丢失
currentMenu.children=state.currentMenu
console.log(currentMenu.children[0].component);
router.addRoute(currentMenu);
},
但是发现问题:import导入的component属性,在复制之后,children里面并没有该属性,因此需要修改
children: [
{
path: "/page1",
name: "page1",
icon: "setting",
meta: {
title: "页面1",
},
url: "/Other/PageOne",
},
{
path: "/page2",
name: "page2",
icon: "setting",
meta: {
title: "页面2",
},
url: "/Other/PageTwo",
},
先通过url保存基本的路径,在添加动态路由的时候,根据实际文件位置来进行导入
//将根据登录用户返回的路由添加到main的子路由中
// currentMenu.children.push(...state.currentMenu);//...state.currentMenu 会造成里面import导入的component属性丢失
//如果是以及菜单,那么菜单名称肯定有路由 如果是二级菜单,一级没有,二级有路由
state.currentMenu.forEach((item) => {
if (item.children) {
item.children = item.children.map((item) => {
item.component = () => import(`@/views${item.url}`);
return item;
});
currentMenu.children.push(...item.children);
} else {
item.component = () => import(`@/views${item.url}`);
currentMenu.children.push(item);
}
});
console.log(currentMenu.children[0].component);
router.addRoute(currentMenu);
import和require
模块化中使用
require是AMD规范引入方式;是运行时调用,苏搜易require理论上可以运用在代码的任何地方;是负值过程,require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量
import是ES6的一个语法标准;是编译时调用,必须放在文件开头;是解构过程
路由跳转用name还是path
尽量用name:前期配置的路由路径后期可能会修改,如果路由跳转使用path,有时候path为‘/’,不利于维护,直接用唯一的name进行跳转
现在已经可以实现:动态路由+登录验证+权限管理
但是还没有将token考虑进去
这一部分的登录验证是mock最简单的时候,当有多个用户时,反正都是后端完成,是否验证成功,我们需要通过form表单将账号和密码传递过去
getdynamicUser: (config) => {
const { username, password } = JSON.parse(config.body);
console.log(JSON.parse(config.body));
//先判断用户是否存在
if (username === "admin" || username === "people") {
//判断账号和密码是否对象
if (username === "admin" && password === "654321") {
return {
code: 200,
message: "登录成功",
data: dynamicUser.filter((item) => {
return item.username === username;
}),
};
} else if (username === "people" && password === "123456") {
return {
code: 200,
message: "登录成功",
data: dynamicUser.filter((item) => {
return item.username === username;
}),
};
} else {
return { code: 404, data: { message: "密码错误" } };
}
token相关
登录所做的事情:
1、获取当前用户对应的菜单栏的菜单,并存储到vuex和cookies中
2、获取当前用户的Token,存储到vuex和cookies中
3、获取当前的菜单生成动态路由
退出登录所做的
1、清除vuex和cookie中的菜单
2、清除vuex和cookie中的token
路由守卫所做的:
因为是后台管理系统,所以在每切换一个路由都需要判断当前token是否存在
sessionStorage,localStorage,cookie
首先,HTTP是无状态协议,不能保存每一次请求的状态,所以需要给客户端增加Cookie来保存客户端状态
Cookie主要用于用户识别和状态管理(比如网页常见的记住密码)
HTML5提供了两种在客户端存储数据的新方法:localStorage和sessionStorage
token和cookie的关系
cookie是客户端存储信息的方式,token是信息本身
HTTP进行数据交换的时候是无状态的,因此每次都需要重新验证身份
Cookie可以作为一个状态保存的状态机,用来保存用户的相关登录状态,当第一次验证通过后,服务器通过set-cookie令客户端将自己的cookie保存起来,当下一次再次发送请求的时候,直接带上cookie即可。而服务器检测到客户端发送的cookie与其保存的cookie值保持一致时,则直接信任该连接,不再进行验证操作
Token:类似cookie的一种验证信息,客户端通过登录验证后,服务器会返回给客户端一个加密的token,当客户端再次向服务器发起连接时,带上token,服务器直接对token进行校验即可完成权限校验
Cookie作为HTTP规范,存在跨域限制。
Token需要自己存储,自己进行发送,不存在跨域限制
安装
npm install js-cookie -s
使用
一般将token存放在cookie以及vuex中
token: "",
},
mutations: {
// 存放token
setToken(state, val) {
state.token = val;
//Cookies.set(key,data) 是 `js-cookie`提供的存放 Cookie 的方法
Cookie.set("token", val);
},
// 获取token
getToken(state) {
state.token = Cookie.get("token");
},
// 清除token
clearToken(state) {
state.token = "";
Cookie.remove("token");
},
当登录验证成功的时候,通过后端返回的数据保存在token中
data: this.dynamicValidateForm,
}).then((res) => {
if (res.data.code == 404) {
this.$message(res.data.data.message);
}
if (res.data.code == 200) {
//后端返回对象中包含token,将token取出放入token
this.$store.commit('setToken',res.data.data[0].token)
//需要把返回的路由,添加到动态路由中去
设置全局路由守卫,进行token验证
Vue.config.productionTip = false
//路由全局守卫
router.beforeEach((to,from,next)=>{
let token=store.state.token
// 过滤登录页,因为去登录页不需要token(防止死循环)
if(!token && to.name!=='login'){
next({name:'login'})
}
else{
next()
}
})
退出登录,所做的:清除动态路由,清除token,跳转到login页面
logOut(){
//登出的时候,需要清除token以及
this.$store.commit('clearToken')
location.reload()//强制页面刷新
}
token实现免密登录
</el-form-item>
<el-form-item>
<el-checkbox label="记住账号" v-model="isRemenber"></el-checkbox>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('dynamicValidateForm')"
>登录</el-button
>
</el-form-item>
isRemenber: false,
动态菜单
const dynamicUser = [
{
name: "管理员",
avatar:
"https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/ccb565eca95535ab2caac9f6129b8b7a~300x300.image",
desc: "管理员 - admin",
username: "admin",
// password: "654321",
token: "rtVrM4PhiFK8PNopqWuSjsc1n02oKc3f",
routes: [
{
path: "/",
name: "home",
icon: "s-home",
meta: {
title: "首页",
},
url: "/Home/Home",
},
{
path: "/mall",
name: "mall",
icon: "video-play",
meta: {
title: "商品管理",
},
url: "/MallManage/MallManage",
},
{
path: "/user",
name: "user",
icon: "user",
meta: {
title: "用户管理",
},
url: "/UserManage/UserManage",
},
{
path: "/other",
icon: "location",
meta: {
title: "其他",
},
url: "/Other/index",
children: [
{
path: "/page1",
name: "page1",
icon: "setting",
meta: {
title: "页面1",
},
url: "/Other/PageOne",
},
{
path: "/page2",
name: "page2",
icon: "setting",
meta: {
title: "页面2",
},
url: "/Other/PageTwo",
},
],
},
],
},
{
name: "普通用户",
avatar:
"https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/6364348965908f03e6a2dd188816e927~300x300.image",
desc: "普通用户 - people",
username: "people",
// password: "123456",
token: "4es8eyDwznXrCX3b3439EmTFnIkrBYWh",
routes: [
{
path: "/",
name: "home",
icon: "s-home",
meta: {
title: "首页",
},
url: "/Home/Home",
},
{
path: "/mall",
name: "mall",
icon: "video-play",
meta: {
title: "商品管理",
},
url: "/MallManage/MallManage",
},
],
},
];
export default {
getdynamicUser: (config) => {
const { username, password } = JSON.parse(config.body);
//先判断用户是否存在
if (username === "admin" || username === "people") {
//判断账号和密码是否对象
if (username === "admin" && password === "654321") {
return {
code: 200,
message: "登录成功",
data: dynamicUser.filter((item) => {
return item.username === username;
}),
};
} else if (username === "people" && password === "123456") {
return {
code: 200,
message: "登录成功",
data: dynamicUser.filter((item) => {
return item.username === username;
}),
};
} else {
return { code: 404, data: { message: "密码错误" } };
}
} else {
return { code: 404, data: { message: "用户不存在" } };
}
},
};
在后台管理项目中,牵涉到权限的东西多数是后端传递过来的数据,前端去展示,就导航菜单而言,不能写死,需要在用户登录之后,发请求获取用户的对应菜单数据,根据对应的数据去展示对应的菜单
</span></el-menu-item
>
</el-menu-item-group>
</el-submenu>
</el-menu>
</template>
el-menu菜单的核心数据:
1、菜单的名字name
点击菜单进行路由跳转的路径path
菜单上小图标icon
菜单不是最内层的菜单,即children是否是空数组,当children为空的时候,就说明到菜单嘴里层了(最里层的菜单children为空数组的时候,点击做路由跳转)
el-menu代码分为两类:有子集和没有子集
有自己:用的是el-submenu标签包含template标签指定名字跟很多个el-menu-item标签
<el-submenu
v-for="(item, index) in hasChildren"
:key="index"
:index="item.meta.title"
>
<template slot="title">
<i :class="'el-icon-' + item.icon"></i>
<span slot="title">{{ item.meta.title}}</span>
</template>
<el-menu-item-group>
<el-menu-item
:index="subItem.path"
v-for="(subItem, subIndex) in item.children"
:key="subIndex"
@click="clickMenu(subItem)"
>
<i :class="'el-icon-' + subItem.icon"></i>
<span slot="title">{{ subItem.meta.title }}</span></el-menu-item
>
</el-menu-item-group>
</el-submenu>
没有子集:直接用很多个el-menu-item标签
<!-- 分为有子路由和无子路由的导航 -->
<el-menu-item
v-for="item in noChildren"
:key="item.name"
:index="item.path"
@click="clickMenu(item)"
>
<i :class="'el-icon-' + item.icon"></i>
<span slot="title">{{ item.meta.title }}</span>
</el-menu-item>