项目介绍:
一个前后端分离的管理系统。前端使用vue框架,后端使用Java的springboot框架。
项目框架:SpringBoot+SpringSecurity+jwt+mybatis-plus+swagger2
环境:JDK1.8
软件: idea
后端教程:Java后端管理系统
前端框架:vue ,vue_cli,element
软件: HBuilder x
1.创建项目vue-cli-3.0
cmd中输入命令:
vue create 项目名
结构:
结构描述见:Vue之vue-cli新和旧版本代码生成的项目区别
2.引入element
官方地址:https://element.eleme.cn/#/zh-CN/component/installation
安装element插件:
npm i element-ui -S
在main.js中引入:
//引入element
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
3.配置路由router
相关资料:【Vue-cli3】路由基础
修改App.vue使用router-view
<template>
<div id="app">
<!-- router-view路由页面组件展示区域 -->
<router-view></router-view>
</div>
</template>
<script>
</script>
<style>
</style>
在main.js中新增路由配置
import Vue from 'vue'
import App from './App.vue'
import router from "./router/index.js" //引入路由配置 在router文件下的index.js
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router //添加路由
}).$mount('#app')
创建路由 src-》router-》index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
// 配置First的路由
{
path: '/',
name: 'Login',
component: () => import("@/components/views/Login.vue"),
},
]
})
4.引入axios和qs配置
安装:
npm install axios --save-dev
npm install qs
main.js添加axios配置:
import axios from 'axios' //引入axios
import qs from 'qs'
Vue.prototype.$axios = axios
var base="http://localhost:8090"
export const POST = (url, params) => {
axios.defaults.transformRequest = [
function (data) {
return qs.stringify(data)
}
];
console.log(url);
return axios.post(`${base}${url}`, params).then(res => {
console.log(res);
if (res.status == 200)
return res.data;
else
console.log("操作失败,服务端出现异常错误!")
})
}
Vue.prototype.$ajax = {
POST(url, params) {
return POST(url, params);
},
}
页面使用:
submitForm() {
var _this=this
this.$ajax
.POST("/login", {
username: this.loginForm.username,
password: this.loginForm.password
}).then((data) => {
console.info(data)
if(data.success){
//处理
_this.$router.push("/home") //跳转
}
})
},
4.登录和主页页面
登录页面Login.vue
4.1 使用全局的css设置整体样式
在 assets-》css文件下创建global.css
html,body,#app{
height: 100%;
margin:0 ;
padding: 0;
}
在man.js中引入:
import './assets/css/global.css' //引入全局样式
4.2 创建Login.vue页面
使用滑块验证插件,使用教程SlideVerify。
<template>
<!-- 登录布局 -->
<div class="login_container">
<!-- 登录区域 -->
<div class="login_box">
<!-- 头像 -->
<div class="avatar_box">
<img src="../assets/img/timg.gif" />
</div>
<!-- 表单 -->
<el-form :model="loginForm" :rules="loginRules" ref="loginForm" label-width="0px" class="login_Form">
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="el-icon-user-solid"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" v-model="loginForm.password" autocomplete="off" prefix-icon="el-icon-lock"></el-input>
</el-form-item>
<el-form-item prop="verifyCode">
<div class="verify_box">
<el-popover
placement="top-start"
width="380"
trigger="click"
v-model="SlideVerify">
<slide-verify ref="slideblock"
:l="42"
:r="10"
:w="380"
:h="185"
@success="onSuccess"
@fail="onFail"
@refresh="onRefresh"
@again="onAgain"
:slider-text="text"
></slide-verify>
<el-button style="width: 410px;" :type="codeType" @click="handleClick" :disabled="disabled" slot="reference">{{TipMsg}}</el-button>
</el-popover>
</div>
</el-form-item>
<el-form-item class="login_btn">
<el-button type="primary" @click="submitForm()">提交</el-button>
<el-button @click="resetForm('loginForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default{
name: 'Login',
data() {
return {
fullscreenLoading: false,
SlideVerify:false,
text: '向右滑',
codeType:"",
TipMsg:"点击验证",
disabled:false,
loginForm: {
username: '',
password: '',
verifyCode: ''
},
loginRules: {
username: [
{ required:true , message: "请输入用户名称",trigger: 'blur' },
{ min :3 ,max:5, message: "长度在 3 到 5 个字符",trigger: 'blur'}
],
password: [
{ required:true , message: "请输入密码",trigger: 'blur' },
{ min :3 ,max:5, message: "长度在 3 到 5 个字符",trigger: 'blur'}
],
verifyCode:[
{ required:true , message: "请输入验证码",trigger: 'blur'}
]
}
};
},
methods: {
onSuccess(){
console.info('成功!')
this.loginForm.verifyCode=true;
this.SlideVerify=false;
this.codeType="success";
this.TipMsg="验证成功";
// this.disabled=true;
},
onFail(){
console.info('失败!')
this.loginForm.verifyCode=false;
this.codeType="danger";
this.TipMsg="验证失败";
},
onRefresh(){
console.info('刷新!')
this.loginForm.verifyCode=false;
this.codeType="";
this.TipMsg="点击验证";
// this.disabled=false;
},
onAgain(){
console.log("检测到非人为操作的哦!");
this.$refs.slideblock.reset();
this.onRefresh();
},
handleClick() {
// 父组件直接可以调用刷新方法
this.$refs.slideblock.reset();
this.TipMsg="验证中";
this.codeType="";
},
submitForm() {
var _this=this
if(_this.loginForm.verifyCode==true){
console.info("验证成功!")
//登录时加载样式
const loading = this.$loading({
lock: true,
// text: 'Loading',
// spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.7)'
});
this.$ajax
.POST("/login", {
username: this.loginForm.username,
password: this.loginForm.password
})
.then((data) => {
console.info(data)
if(data.success){
_this.$message.success("登录成功!");
console.info("存储token")
localStorage.setItem("token",data.data.token)
loading.close();//关闭加载样式
_this.$router.push("/")
} else{
_this.$message.error("登录失败!账号密码错误!");
_this.onRefresh();
}
})
}else{
this.$message.info("请点击验证!");
}
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style>
.el-loading-spinner {
/* // 这个是自己想设置的 gif 加载动图 */
background-image:url('../assets/img/loading.gif');
/* //设置背景图 不重复 */
background-repeat: no-repeat;
height:10%;
/* //设置背景 定位 为居中 */
background-position:center;
/* //覆盖 element-ui 默认的 50% 因为此处设置了height:100%,所以不设置的话,会只显示一半,因为被top顶下去了 */
top:0;
}
.el-loading-spinner .circular {
/* //隐藏 之前 element-ui 默认的 loading 动画 */
display: none;
}
</style>
<!-- 内部样式 -->
<style lang="less" scoped >
.login_container{
width: 100%;
height: 100%;
background-color:aquamarine;
}
.login_box{
width: 450px;
height: 380px;
background-color: #FFFFFF;
border-radius: 3px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.avatar_box{
width: 130px;
height: 130px;
border: 1px solid #EEEEEE;
border-radius: 50%;
padding: 10px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
margin: -65px auto;
background-color: #FFFFFF;
img{
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eeeeee;
}
}
.login_Form{
position: absolute;
bottom: 0px;
width: 100%;
padding: 0px 20px;
box-sizing: border-box;
.login_btn{
display: flex;
justify-content: flex-end;
}
}
.verify_box{
display: flex;
.verify_code{
width: 70%;
justify-content: left;
}
.verify_img{
width: 30%;
height: 45px;
justify-content: flex-end;
}
}
</style>
使用localStorage.setItem(“token”,token) 存储token,利用拦截器每次请求带上token ,过期删除跳转登录页面。
4.3 创建Home.vue页面
首先在路由router-》index.js中配置
export default new Router({
routes: [
// 配置First的路由
{
path: '/',
name: 'Login',
component: () => import("@/components/views/Login.vue"),
},
{
path: '/home',
name: 'Home',
component: () => import("@/components/views/Home.vue"),
},
]
})
使用element的Container 布局容器
<template>
<el-container class="el-container">
<!-- 头部布局 -->
<el-header>
<!-- logo和项目名 -->
<div class="left_box">
<img src="../../assets/img/timg.gif" />
<span>集元库房管理系统</span>
</div>
<!-- 用户登录展示头像 -->
<div class="right_box">
<i class="el-icon-full-screen" @click="toggleFullscreen"></i>
<!-- 下拉菜单 -->
<el-dropdown>
<img src="../../assets/img/timg.gif" />
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-user">个人信息</el-dropdown-item>
<el-dropdown-item icon="el-icon-edit-outline">修改密码</el-dropdown-item>
<el-dropdown-item icon="el-icon-switch-button"><a href="/">退出登录</a></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<el-container>
<el-aside :width="isCollapse?'60px':'200px'" >
<!--展开/收起-->
<div class="toggle_box" @click="toggleCollapse">|||</div>
<el-menu
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="#001529"
text-color="#fff"
active-text-color="#ffd04b"
:collapse="isCollapse"
:default-active="activePath"
:collapse-transition="false"
:unique-opened="true"
:router="true">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-s-tools"></i>
<span>系统管理</span>
</template>
<el-menu-item-group>
<el-menu-item index="/user"><i class="el-icon-user" ></i>用户管理</el-menu-item>
<el-menu-item index="1-2"><i class="el-icon-star-off"></i>权限管理</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-message-solid"></i>
<span>日志管理</span>
</template>
<el-menu-item-group>
<el-menu-item index="2-1"><i class="el-icon-view"></i>sql日志</el-menu-item>
<el-menu-item index="2-2"><i class="el-icon-edit"></i>用户日志</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-main>
<!-- 路由视图 -->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
export default{
name: 'Main',
data(){
return {
isCollapse: false,
activePath: '',
}
},
components: {
},
methods: {
toggleCollapse () {
this.isCollapse = !this.isCollapse
},
handleOpen (key, keyPath) {
console.log(key, keyPath)
},
handleClose (key, keyPath) {
console.log(key, keyPath)
},
toggleFullscreen() {
let _this = this;
let el = document.documentElement;
if (document.fullscreenElement === null) {
_this.openFullscreen(el);
} else {
_this.quitFullscreen();
}
},
openFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
// IE11
element.msRequestFullscreen();
}
},
quitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
},
getKeyDown() {
let _this = this;
if (event.keyCode === 122) {
event.preventDefault() || (event.returnValue = false);
_this.toggleFullscreen(); // 触发全屏的按钮
} else if (event.keyCode === 27) {
if (document.fullscreenElement !== null) {
_this.quitFullScreen();
}
}
}
},
mounted() {
window.addEventListener("keydown", this.getKeyDown);
}
}
</script>
<style lang="less" scoped>
/*设置整个容器*/
.el-container{
width: 100%;
height: 100%;
}
/*头部布局*/
.el-header {
background-color: #001529;
display: flex;
justify-content: space-between;
padding-left:0 ;
color: #FFFFFF;
align-items: center;
font-size: 20px;
}
/*侧边栏*/
.el-aside {
background-color: #001529;
color: #333;
//去掉边框
.el-menu {
border-right: none;
}
/*展开/收起*/
.toggle_box {
background-color: chartreuse;
font-size: 15px;
font-weight: bold;
line-height: 24px;
color: #FFFFFF;
letter-spacing: 0.2em;
text-align: center;
cursor: pointer;
}
}
/*内容*/
.el-main {
background-color: #E9EEF3;
}
body > .el-container {
margin-bottom: 40px;
}
/*左边logo和标题*/
.left_box{
display: flex;
align-items: center;
/*logo*/
img{
width: 60px;
height: 60px;
margin: 0px 0px 10px 15px;
}
/*标题*/
span{
margin-left: 15px;
}
}
/*右边的登录头像*/
.right_box{
/*全屏按钮、主题换色*/
// el-icon-full-screen
// 头像
.el-dropdown > img{
height: 60px;
width: 60px;
border-radius:50% ;
background-color: #FFFFFF;
margin:0px 15px 0px 0px ;
}
}
// 下拉菜单
.el-dropdown-link {
cursor: pointer;
color: #409EFF;
}
.el-icon-arrow-down {
font-size: 12px;
}
a {
text-decoration: none;
color: #000000;
}
</style>
登录成功跳转home页面
5.token使用
在登录成功后使用localStorage.setItem(“token”,token) 存储,每次请求由拦截器带上token。
5.1 配置请求拦截器
在main.js中
let token="";
axios.defaults.headers.common['token'] = token;
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
let token=localStorage.getItem('token')
console.info("每次带token")
console.info(token)
if(token)
config.headers.common['token'] = token;
return config;
}, function (error) {
// Do something with request error
Message.error('网络连接异常,请稍后再试!');
console.info("error: ");
console.info(error);
return Promise.reject(error);
});
// 添加一个响应拦截器
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
// Do something with response error
if (error.response.status == 404) {
//找不到请求的服务
Message.error('找不到请求的服务!');
}
else if (error.response.status == 401) {
//没有token需要登录
Message.error('登录凭证失效,请重新登录!');
window.location.href = "/login";
} else if (error.response.status == 403) {
Message.error('token过期,请重新登录!');
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
})
进行登录:
这里是在配置了初始token为空,可以选择不带
5.2 创建User.vue页面
在路由router-》index.js中配置user路由
放在home下为子标签, Container 布局容器的main部分会改变显示
{
path: '/home',
name: 'Home',
component: () => import("@/components/views/Home.vue"),
// 子标签:children。 页面使用<router-view></router-view> 显示
children :[
{
path: '/', //默认显示
name: 'User',
//使用() => import('xx')引入
component: () => import('../components/users/User.vue')
},
]
}
User.vue
<template>
<div>
<!-- 面包屑 -->
<el-breadcrumb separator-class="el-icon-arrow-right" style="padding-left: 10px;padding-bottom: 10px;">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>系统管理</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
</el-breadcrumb>
<!-- 用户列表卡片 -->
<el-card class="box-card">
<el-form :inline="true" :model="UserInfoVo" class="demo-form-inline">
<el-form-item label="部门" label-width="70px" >
<el-select v-model="UserInfoVo.depId" clearable placeholder="请选择" >
<el-option
v-for="item in departments"
:key="item.dpId"
:label="item.departmentName"
:value="item.dpId" >
<span style="float:left">{{item.departmentName}}</span>
<span style="float: right;color: #409EFF;font-size: 13px;">
<span class="el-tag el-tag--success el-tag--mini el-tag--plain">{{item.deptCount}}</span>
</span>
</el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="用户名" label-width="70px">
<el-input clearable v-model="UserInfoVo.userName" placeholder="请输入用户名"></el-input>
</el-form-item> -->
<el-form-item label="邮箱" label-width="70px">
<el-input v-model="UserInfoVo.email" clearable placeholder="请输入邮箱"></el-input>
</el-form-item>
<el-radio-group v-model="UserInfoVo.sex" label-width="70px">
<el-radio :label="0">男</el-radio>
<el-radio :label="1">女</el-radio>
<el-radio label="">全部</el-radio>
</el-radio-group>
<br />
<el-form-item label="昵称" label-width="70px">
<el-input clearable v-model="UserInfoVo.nickname" placeholder="请输入昵称"></el-input>
</el-form-item>
<!-- 按钮 -->
<el-form-item style="margin-left: 10px;">
<el-row>
<el-button icon="el-icon-refresh" @click="resetUserInfoVO()">重置</el-button>
<el-button type="primary" icon="el-icon-search" @click="findLists()">查询</el-button>
<el-button type="success" icon="el-icon-plus" @click="show">添加</el-button>
<el-button type="warning" icon="el-icon-download">导出</el-button>
</el-row>
</el-form-item>
</el-form>
<!-- // 表格显示 -->
<el-table
:data="userList"
border
style="width: 100%;height:600px;">
<el-table-column
prop="userId"
label="#"
width="150">
</el-table-column>
<el-table-column
sortable
prop="departmentName"
label="部门"
width="150">
</el-table-column>
<el-table-column
sortable
prop="nickname"
label="昵称"
width="150">
</el-table-column>
<el-table-column
prop="sex"
label="性别"
width="150">
<template slot-scope="scope">{{scope.row.sex==0?'男':(scope.row.sex==1?'女':'保密')}}</template>
</el-table-column>
<el-table-column
prop="birthday"
label="生日"
width="150" >
<template slot-scope="scope">{{scope.row.birthday| dateYMDFormat}}</template>
</el-table-column>
<el-table-column
prop="phone"
label="手机号"
width="150">
</el-table-column>
<el-table-column
prop="email"
label="邮箱"
width="150">
</el-table-column>
<el-table-column
prop="delflag"
label="是否禁用"
width="100">
<template slot-scope="scope">
<el-switch
v-model="scope.row.delflag"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
</template>
</el-table-column>
<el-table-column
label="操作">
<el-button type="primary" size="mini" icon="el-icon-edit"></el-button>
<el-button type="danger" size="mini" icon="el-icon-delete"></el-button>
<el-button type="warning" size="mini" icon="el-icon-s-tools"></el-button>
</el-table-column>
</el-table>
<!-- 新增编辑弹框子组件 -->
<!--<user-info-add :addOrUpdateVisible="addOrUpdateVisible" @changeShow="showAddOrUpdate" ref="UserInfoAdd"></user-info-add> -->
<!-- 分页 -->
<div class="block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 30, 40]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-card>
</div>
</template>
<script>
// import Home from "../views/Home"
import UserInfoAdd from './UserInfoAdd.vue'
export default{
name: 'Main',
data() {
return {
value:'',
//每页显示条数
pageSize:10,
//总记录条数
total:7,
//初始页数
currentPage:1,
// 用户信息集合 静态数据
userList: [{
userId: '1',
departmentName: '研发部',
nickname: '张小虎',
sex:1,
birthday:"2016-05-02",
phone:"13452850922",
email:"13452850922@qq.com"
}],
departments: [],
UserInfoVo:{
"depId": "",
"departmentName": "",
"email": "",
"nickname": "",
"phone": "",
"sex": "",
"userName": ""
},
// 控制新增编辑弹窗的显示与隐藏
addOrUpdateVisible: false
}
},
//创建组件时调用方法
created(){
this.findLists();
this.findDeptAndCount();
},
// 使用子组件
components:{
UserInfoAdd
},
methods: {
//当每页条数改变时
handleSizeChange(val) {
this.pageSize=val;
this.findLists();
console.log(`每页 ${val} 条`);
},
// 当页码改变时
handleCurrentChange(val) {
this.currentPage=val;
this.findLists();
console.log(`当前页: ${val}`);
},
// 异步
async findLists(){
var _this=this
console.log(this.UserInfoVo);
this.$ajax
.POST("/department/findLists", {
currentPage: this.currentPage,
pageSize: this.pageSize,
UserInfoVo:this.UserInfoVo
})
.then((data) => {
console.info(data)
if(data.success){
_this.userList = data.data.userInfoList;
_this.total=data.data.total;
}
})
},
async findDeptAndCount(){
var _this=this;
this.$ajax
.POST("/department/findDeptAndCount", {
currentPage: this.currentPage,
pageSize: this.pageSize
})
.then((data) => {
console.info(data)
if(data.success){
_this.departments = data.data.departments;
}
})
},
// 重置查询条件
resetUserInfoVO(){
this.UserInfoVo.depId="",
this.UserInfoVo.departmentName= "",
this.UserInfoVo.email= "",
this.UserInfoVo.nickname= "",
this.UserInfoVo.phone= "",
this.UserInfoVo.sex= "",
this.UserInfoVo.userName= ""
this.findLists();
},
// 按钮点击事件 显示新增编辑弹窗组件
show(){
this.addOrUpdateVisible = true
},
// 监听 子组件弹窗关闭后触发,有子组件调用
showAddOrUpdate(data){
if(data === 'false'){
this.addOrUpdateVisible = false
}else{
this.addOrUpdateVisible = true
}
}
}
}
</script>
<style>
</style>
配置:Vue element el-table-column标签中对日期进行格式化(全局过滤器)
6.使用elment标签页
相关文章:
1.elementui tab标签管理路由页面。
2.Vue之标签页。
3.Vue之Element标签页保留用户操作缓存。
效果:
6.1 创建标签页路由store.js
import Vue from 'vue'
import Vuex from 'vuex'
import store from './store'
import router from './router.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
openTab:[],//所有打开的路由
activeIndex:'/main', //当前激活状态
curContextTabId:"", //右击当前标签页id
excludeList:[],//keep-alive的黑名单
includeList:[]//keep-alive的白名单
},
mutations: {
// 添加tabs
add_tabs (state, data) {
this.state.openTab.push(data);
},
// 删除tabs
delete_tabs (state, route) {
let index = 0;
for (let option of state.openTab) {
if (option.route === route) {
break;
}
index++;
}
this.state.openTab.splice(index, 1);
},
// 设置当前激活的tab
set_active_index (state, index) {
this.state.activeIndex = index;
},
// 保存右键点击tab的id
saveCurContextTabId(state, curContextTabId) {
this.state.curContextTabId = curContextTabId
},
// 关闭所有标签
closeAllTabs(state) {
this.state.openTab = [];
//想实现标签和页面同步,以下三句代码
store.commit('add_tabs', {route: '/main', name: '首页'}); //添加进入标签页数组
store.commit('set_active_index', '/main'); // 激活/main的标签页
router.push({path: '/main'});// 跳转路由页面 显示页面
},
// 关闭其它标签页
closeOtherTabs(state, par) {
let tabs = state.openTab;
let length = tabs.length;
let curContextTabId = state.curContextTabId;
let activeTabItem = state.activeIndex
// console.log("当前激活:"+activeTabItem)
// console.log("右击选中:"+curContextTabId)
let id; // 右键点击时的tab在整个tabs数组中的id
let curId // 左键点击时的tab在整个tabs数组中的id
tabs.forEach((tab, index) => {
if (tab.route == curContextTabId) {
id = index
}
if (tab.route == activeTabItem) {
curId = index
}
})
// console.log("激活:"+activeTabItem+" id:"+id+"|| 右击选中:"+curContextTabId+" curid:"+ curId)
store.commit('set_active_index', curContextTabId);//激活显示当前页面
router.push({path: curContextTabId});//激活路由转换页面
if (par == "left") {
// console.info("关闭左边! 显示当前标签:"+curContextTabId)
this.state.openTab= state.openTab.slice(id, length) //截取当前标签到末尾标签
//使首页一直存在第一位置
if(curContextTabId !="/main")
{
this.state.openTab.unshift({route: '/main', name: '首页'}); //导入首页在数组头部
}
}
if (par == "right") {
// console.info("关闭右边! 显示当前标签:"+curContextTabId)
this.state.openTab = state.openTab.slice(0, id + 1) //截取0到当前选择标签
}
if (par == "other") {
// console.info("关闭其他! 激活当前标签:"+curContextTabId)
this.state.openTab = [state.openTab[id]] //只留当前标签和首页
if(curContextTabId !="/main")
{
this.state.openTab.unshift({route: '/main', name: '首页'});
}
}
}
},
actions: {
}
})
6.2 修改Home页面
新增:标签页和标签页右键菜单功能、用户操作缓存、关闭标签页清除缓存。
使用以下布局结合标签页
<template>
<el-container class="el-container">
<!-- 头部布局 -->
<el-header>
<nav-top></nav-top>
</el-header>
<!-- 左侧导航栏 -->
<el-container>
<slider></slider>
<el-main>
<!-- 内容区 -->
<div class="app-wrap">
<!-- 此处放置el-tabs 标签代码 -->
<div >
<el-tabs
v-model="activeIndex"
type="border-card"
closable
v-if="openTab.length"
@tab-click='tabClick'
@tab-remove='tabRemove'
@contextmenu.prevent.native="openContextMenu($event)"
>
<el-tab-pane
:key="item.name"
v-for="(item, index) in openTab"
:label="item.name"
:name="item.route"
>
</el-tab-pane>
<div class="content-wrap">
<!-- 缓存页面不刷新 -->
<keep-alive :include="includeList">
<router-view></router-view>
</keep-alive>
</div>
</el-tabs>
</div>
</div>
<ul
v-show="contextMenuVisible"
:style="{left:left+'px',top:top+'px'}"
class="contextmenu"
>
<li @click="closeAllTabs">关闭所有</li>
<li @click="closeOtherTabs('left')">关闭左边</li>
<li @click="closeOtherTabs('right')">关闭右边</li>
<li @click="closeOtherTabs('other')">关闭其他</li>
</ul>
</el-main>
</el-container>
</el-container>
</template>
<script>
// @ is an alias to /src
import Slider from '@/components/Slider.vue'
import navTop from '@/components/Header.vue'
import store from '../store.js'
export default {
name: 'Home',
components: {
Slider,navTop,store
},
data(){
return{
contextMenuVisible:false,
currentContextTabId:'',
left:0,
top:0,
includeList:[]
}
},
mounted () {
//监控标签页点击空白事件
document.body.addEventListener("click", this.closeContextMenu);
window.addEventListener("keydown", this.getKeyDown);
// 刷新时以当前路由做为tab加入tabs
// 当前路由不是首页时,添加首页以及另一页到store里,并设置激活状态
// 当当前路由是首页时,添加首页到store,并设置激活状态
if (this.$route.path != '/' && this.$route.path != '/main') {
// console.log('1');
this.$store.commit('add_tabs', {route: '/main', name: '首页'}); //添加到标签页中
this.$store.commit('set_active_index', '/main');//激活显示页面
this.$store.commit('add_tabs', {route: this.$route.path , name: this.$route.name });
this.$store.commit('set_active_index', this.$route.path);
} else {
// console.log('2');
this.$store.commit('add_tabs', {route: '/main', name: '首页'});
this.$router.push('/');
}
this.updateIncludeList();
},
methods:{
// 标签页右击菜单
openContextMenu(e) {
//console.log(e.srcElement);
if (e.srcElement.id) {
let currentContextTabId = e.srcElement.id.split("-")[1];
this.contextMenuVisible = true;
// console.info("标签id:"+currentContextTabId);
this.$store.commit("saveCurContextTabId", currentContextTabId);
this.currentContextTabId=currentContextTabId;
this.left = e.clientX;
this.top = e.clientY + 10;
}
},
// 关闭所有标签页
closeAllTabs() {
this.$store.commit("closeAllTabs");
this.contextMenuVisible = false;
this.updateIncludeList();
},
// 关闭其它标签页
closeOtherTabs(par) {
this.$store.commit("closeOtherTabs", par);
this.contextMenuVisible = false;
this.updateIncludeList();
},
// 关闭右击菜单
closeContextMenu() {
this.contextMenuVisible = false;
},
//tab标签点击时,切换相应的路由
tabClick(tab){
// console.log("tab",tab);
console.log('active',this.activeIndex);
this.$router.push({path: this.activeIndex});
},
//移除tab标签
tabRemove(targetName){
console.log("tabRemove",targetName);
this.blackList=targetName;
//首页不删
if(targetName == '/'||targetName == '/main'){
return
}
this.$store.commit('delete_tabs', targetName);
if (this.activeIndex === targetName) {
// 设置当前激活的路由
// console.info("当前标签:"+this.openTab.length)
if (this.openTab && this.openTab.length >= 1) {
// console.log('=============',this.openTab[this.openTab.length-1].route)
this.$store.commit('set_active_index', this.openTab[this.openTab.length-1].route);
this.$router.push({path: this.activeIndex});
} else {
this.$router.push({path: '/'});
}
}
this.updateIncludeList();
},
//更新缓存白名单
updateIncludeList(){
var arr=[];
for (var i = 0; i < this.openTab.length; i++) {
var temp=this.openTab[i].route.replace("/","");
console.log("temp:"+temp);
arr.push(temp)
}
this.includeList=arr;
}
},
computed: {
openTab () {
return this.$store.state.openTab;
},
activeIndex:{
get () {
return this.$store.state.activeIndex;
},
set (val) {
this.$store.commit('set_active_index', val);
}
}
},
watch:{
'$route'(to,from){
//判断路由是否已经打开
//已经打开的 ,将其置为active
//未打开的,将其放入队列里
let flag = false;
for(let item of this.openTab){
console.log("item.name",item.name)
// console.log("t0.name",to.name)
if(item.name === to.name){
// console.log('to.path',to.path);
this.$store.commit('set_active_index',to.path)
flag = true;
break;
}
}
if(!flag){
// console.log('to.path',to.path);
this.$store.commit('add_tabs', {route: to.path, name: to.name});
this.$store.commit('set_active_index', to.path);
}
this.updateIncludeList();
}
}
}
</script>
<style >
/*设置整个容器*/
.el-container{
width: 100%;
height: 100%;
}
/*头部布局*/
.el-header {
background-color: #001529;
display: flex;
justify-content: space-between;
padding-left:0 ;
color: #FFFFFF;
align-items: center;
font-size: 20px;
}
/*侧边栏*/
.el-aside {
background-color: #001529;
color: #333;
/* //去掉边框 */
.el-menu {
border-right: none;
}
/*展开/收起*/
.toggle_box {
background-color: chartreuse;
font-size: 15px;
font-weight: bold;
line-height: 24px;
color: #FFFFFF;
letter-spacing: 0.2em;
text-align: center;
cursor: pointer;
}
}
/*内容*/
.el-main {
background-color: #E9EEF3;
}
body > .el-container {
margin-bottom: 40px;
}
/*左边logo和标题*/
.left_box{
display: flex;
align-items: center;
/*logo*/
img{
width: 60px;
height: 60px;
margin: 0px 0px 10px 15px;
}
/*标题*/
span{
margin-left: 15px;
}
}
/*右边的登录头像*/
.right_box{
/*全屏按钮、主题换色*/
// el-icon-full-screen
// 头像
.el-dropdown > img{
height: 60px;
width: 60px;
border-radius:50% ;
background-color: #FFFFFF;
margin:0px 15px 0px 0px ;
}
}
/* // 下拉菜单 */
.el-dropdown-link {
cursor: pointer;
color: #409EFF;
}
.el-icon-arrow-down {
font-size: 12px;
}
a {
text-decoration: none;
color: #000000;
}
/* 标签页右击菜单样式 */
.contextmenu {
width: 100px;
margin: 0;
border: 1px solid #ccc;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 14px;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.2);
}
.contextmenu li {
margin: 0;
padding: 7px 16px;
}
.contextmenu li:hover {
background: #f2f2f2;
cursor: pointer;
}
</style>
6.3 Header部分-Header.vue
<template>
<div class="header">
<!-- logo和项目名 -->
<div class="left_box">
<img src="../assets/img/timg.gif" />
<span>XX管理系统</span>
</div>
<!-- 用户登录展示头像 -->
<div class="right_box">
<i class="el-icon-full-screen" @click="toggleFullscreen"></i>
<!-- 下拉菜单 -->
<el-dropdown>
<img src="../assets/img/timg.gif" />
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-user">个人信息</el-dropdown-item>
<el-dropdown-item icon="el-icon-edit-outline">修改密码</el-dropdown-item>
<el-dropdown-item icon="el-icon-switch-button" ><span @click='logout'>退出登录</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
export default {
name:'navTop',
methods:{
logout(){
console.info("退出登录!清除token")
this.$store.state.openTab = [];
// this.$store.state.activeIndex = '/main';
//清除token
localStorage.removeItem('token');
this.$router.push('/login')
},
toggleFullscreen() {
let _this = this;
let el = document.documentElement;
if (document.fullscreenElement === null) {
_this.openFullscreen(el);
} else {
_this.quitFullscreen();
}
},
openFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
// IE11
element.msRequestFullscreen();
}
},
}
}
</script>
<style lang="less" scoped>
/*头部布局*/
.header {
background-color: #001529;
display: flex;
justify-content: space-between;
padding-left:0 ;
color: #FFFFFF;
align-items: center;
font-size: 20px;
width: 100%;
}
/*左边logo和标题*/
.left_box{
display: flex;
align-items: center;
/*logo*/
img{
width: 60px;
height: 60px;
margin: 0px 0px 10px 15px;
}
/*标题*/
span{
margin-left: 15px;
}
}
/*右边的登录头像*/
.right_box{
/*全屏按钮、主题换色*/
/* // el-icon-full-screen */
/* // 头像 */
.el-dropdown > img{
height: 60px;
width: 60px;
border-radius:50% ;
background-color: #FFFFFF;
margin:0px 15px 0px 0px ;
}
}
</style>
6.4 Asider部分-Slider.vue
<template>
<el-aside class="aside" :width="isCollapse?'60px':'200px'" >
<!--展开/收起-->
<div class="toggle_box" @click="toggleCollapse">|||</div>
<el-menu
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="#001529"
text-color="#fff"
active-text-color="#ffd04b"
:collapse="isCollapse"
:default-active="activePath"
:collapse-transition="false"
:unique-opened="true"
:router="true">
<el-menu-item index="/main" >
<i class="el-icon-menu"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-submenu v-for="(item,index) in menuList"
:key="index"
:index='item.id'
v-if="item.show && item.children!=''">
<!-- 主菜单 -->
<template slot="title">
<i :class="item.icon"></i>
<span>{{item.title}}</span>
</template>
<!-- 子菜单 -->
<el-menu-item-group >
<el-menu-item v-for="child in item.children"
:key="child.id"
:index='child.index'
v-if="child.show" >
<i :class="child.icon"></i><span>{{child.childtitle}}</span>
</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
</template>
<script>
export default {
name: 'Slider',
data(){
return {
isCollapse: false,
activePath: '',
menuList:[
{
id:'1',
title: '系统管理',
icon:'el-icon-s-tools',
show:true,
children:[
{
index:'/user',
childtitle:'用户管理',
icon:'el-icon-user',
show:true,//是否显示子菜单页面
},
{
index:'/page2',
childtitle:'权限管理',
icon:'el-icon-star-off',
show:true,
},
]
},
{
id:'2',
title: '日志管理',
icon:'el-icon-message-solid',
show:true,
children:[
{
index:'/page3',
childtitle:'sql日志',
icon:'el-icon-view',
show:true,
},
{
index:'/page4',
childtitle:'用户日志',
icon:'el-icon-edit',
show:true,
},
]
}
]
}
},
methods:{
toggleCollapse () {
this.isCollapse = !this.isCollapse
},
handleOpen (key, keyPath) {
// console.log(key, keyPath)
},
handleClose (key, keyPath) {
// console.log(key, keyPath)
},
quitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
},
getKeyDown() {
let _this = this;
if (event.keyCode === 122) {
event.preventDefault() || (event.returnValue = false);
_this.toggleFullscreen(); // 触发全屏的按钮
} else if (event.keyCode === 27) {
if (document.fullscreenElement !== null) {
_this.quitFullScreen();
}
}
},
}
}
</script>
<style lang="less" scoped>
/*侧边栏*/
.el-aside {
background-color: #001529;
color: #333;
/* //去掉边框 */
.el-menu {
border-right: none;
}
/*展开/收起*/
.toggle_box {
background-color: chartreuse;
font-size: 15px;
font-weight: bold;
line-height: 24px;
color: #FFFFFF;
letter-spacing: 0.2em;
text-align: center;
cursor: pointer;
.i{
font-size: 15px;
}
}
}
</style>
6.5 main.js修改
新增:限制标签页条数,路由拦截跳转。
import Vue from 'vue'
import App from './App.vue'
import router from './router' //引入路由
import store from './store' //引入标签
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import './assets/css/global.css' //引入全局样式
//验证码插件
import SlideVerify from 'vue-monoplasty-slide-verify';
Vue.use(SlideVerify);
Vue.use(ElementUI);
Vue.config.productionTip = false
import { Message } from 'element-ui';
//引入日期标签格式化插件
import moment from 'moment'
Vue.use(require('vue-moment'));
Vue.prototype.moment = moment
Vue.filter('dateYMDHMSFormat',function(dateStr,pattern='YYYY-MM-DD HH:mm:ss'){
return moment(dateStr).format(pattern);
})
import axios from 'axios' //引入axios
import qs from 'qs'
var base="http://localhost:8090"
export const POST = (url, params) => {
axios.defaults.transformRequest = [
function (data) {
return qs.stringify(data)
}
];
console.log(url);
return axios.post(`${base}${url}`, params).then(res => {
console.log(res);
if (res.status == 200)
return res.data;
else
this.$message.error("操作失败,服务端出现异常错误!")
})
}
Vue.prototype.$ajax = {
POST(url, params) {
return POST(url, params);
},
}
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
let token=localStorage.getItem('token')
// console.info("每次带token")
// console.info(token)
if(token)
config.headers.common['token'] = token;
return config;
}, function (error) {
// Do something with request error
Message.error('网络连接异常,请稍后再试!');
console.info("error: ");
console.info(error);
return Promise.reject(error);
});
// 添加一个响应拦截器
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
// Do something with response error
if (error.response.status == 404) {
//找不到请求的服务
Message.error('找不到请求的服务!');
}
else if (error.response.status == 401) {
//没有token需要登录
Message.error('登录凭证失效,请重新登录!');
localStorage.removeItem("token");
window.location.href = "/login";
} else if (error.response.status == 403) {
Message.error('token过期,请重新登录!');
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
})
router.beforeEach((to, from, next) => {
console.info("当前标签数:"+store.state.openTab.length)
//限制最大标签页数 超过不跳转路由~
if(store.state.openTab.length>12){
Message.info('标签页已达到最大12页');
return
}
//根据字段判断是否进行路由过滤(即是否需要登录才能访问)
if (localStorage.getItem('token') != null || to.name == 'login') {
next()
} else {
console.info("token失效跳转登录页面")
next({
path: '/login',
});
}
});
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
6.6 router.js修改
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import main from './views/main.vue'
import user from './views/user.vue'
import page2 from './views/page2.vue'
import page3 from './views/page3.vue'
import page4 from './views/page4.vue'
import login from './views/Login'
Vue.use(Router)
export default new Router({
routes: [
{
path:'/login',
component:login,
name:'login'
},
{
path: '/',
component: Home,
redirect: 'main',
children: [
{
path: 'main',
name: '首页',
component: main
},
{
path: 'user',
name: '用户管理',
component: user
},
{
path: 'page2',
name: 'page2',
component: page2
},
{
path: 'page3',
name: 'page3',
component: page3
},
{
path: 'page4',
name: 'page4',
component: page4
}
]
}
]
})
const originalPush = Router.prototype.push
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
到此vue主体框架完成…
github地址:https://github.com/123ac/tabRouter
– 未完待续