Tab、Menu联动,使用一个文件生成多个内容相同tab名不同的view
问题:组件使用同一个路由,路由未发生变化,页面未进行刷新,多个视图缓存第一个视图内容
最终效果:
Tab、Menu联动
vuex(store.js)
Tabs/index.vue
<template>
<div class="menu-page">
<el-backtop :visibilityHeight="50"></el-backtop>
<!-- unique-opened="true" text-color="#09448A"-->
<div class="flex">
<!-- 左侧菜单 -->
<el-menu :unique-opened="true" :collapse="collapse" class="el-menu-vertical-demo" background-color="#FFF" text-color="#666" active-text-color="#89B5E9" :default-active="menuActive" router>
<el-submenu :index="index+++''" v-for="(menu,index) in menuList" :key="index">
<template slot="title">
<i :class="menu.nodeClass"></i>
<span class="nodeName">{{ menu.nodeName }}</span>
</template>
<el-menu-item :index="c.runScript" v-for="(c,indexs) in menu.pdDMenuVO" :key='indexs'>{{ c.nodeName }}</el-menu-item>
</el-submenu>
</el-menu>
<div class="main-r">
<div class="header" :style="headerWidth">
<!-- logo -->
<!-- <div class="logoReplace"></div> -->
<div class="logo" @click="routerIndex"></div>
<!-- 折叠按钮 -->
<div class="collapse-btn" @click="collapseChage">
<i v-if="!collapse" class="el-icon-s-fold"></i>
<i v-else class="el-icon-s-unfold"></i>
</div>
<div class="header-right">
<div class="header-user-con">
<!-- 用户头像 -->
<div class="user-avator">
<el-avatar size="small" src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"></el-avatar>
</div>
<!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" trigger="click" @command="handleCommand">
<span class="el-dropdown-link">
{{ this.uname }}
<i class="el-icon-caret-bottom"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item style="padding:0;"><span style="padding:5px 10px;display:inline-block;" @click="outLogin">退出登录</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div class="header-div"></div>
</div>
<div style="height:60px;"></div>
</div>
<!-- 主内容区域 -->
<el-main :style="mainStyle">
<div class="mainUrl_box">
<!-- tab区 -->
<el-tabs type="card" v-model="active_name" @tab-click="tabClick" @tab-remove="tabRemove">
<el-tab-pane :key="item.title" v-for="item in $store.state.routerMeunMessage" :label="item.title"
:name="item.title" :closable="item.title == '主页' ? false : true">
<keep-alive>
<router-view class="routerView" v-if="isRouterAlive" :deptCode='deptCode' />
</keep-alive>
</el-tab-pane>
</el-tabs>
</div>
</el-main>
</div>
</div>
</template>
<script>
import bus from '../../utils/bus';
import store from '@/store';
export default {
provide() {
return {
reload: this.reload
};
},
data() {
return {
//保存匹配到的菜单项的信息
routerMeunMessage: [],
//将监视到的路由赋值给active_name作为tab标签页的v-model绑定值,用来控制选中对应name的选项卡,本人给el-tab-pane的name赋予路由值
//el-tabs中v-model绑定的值对应el-tab-pane中的name值,以此来指定选中的选项卡,选中的选项卡会被追加class,即样式修改,具体追加的看开发者工具
active_name: '主页',
//当前激活菜单的 index,需要设置默认值
menuActive: '/index',
url: '',
uname: sessionStorage.getItem('username') ? JSON.parse(sessionStorage.getItem('username')) : '',
nodeName: '',
//主内容区样式
mainStyle: {
position: "absolute",
top: "60px",
left: "200px",
right: "0px",
'transition': 'all .3s'
},
headerWidth: {
'transition': 'all .3s'
},
collapse: true,
message: 0,
deptCode: '',
isRouterAlive: true,
nameFlag: ''
};
},
computed: {
menuList: {
get() { return this.$store.state.menuList; },
set(v) { this.$store.commit('menuListLoad', v); }
},
username: {
get() { return this.$store.state.operator; },
set(v) { this.$store.commit('updateUserCode', v); }
}
},
//监听函数
watch: {
// 通过监听路由来控制tab项的高亮,实现点击菜单项时对应的tab项也高亮
$route: {
handler(nowPath, oldPath) {
console.log(nowPath);
//导航守卫,可以获取router中的对应路由的信息
let stateTitle = []//保存遍历的已保存的导航title
let dict = {}
const title = nowPath.meta.title
const path = nowPath.path
//判断是否有title
if (title) {
dict.title = title
dict.path = path
let stateRouterMeunMessage = store.state.routerMeunMessage
//遍历保存到vuex中的state的导航数据的title值,并添加到临时列表保存用于判断tab是否已经存在
for (let item of stateRouterMeunMessage) {
stateTitle.push(item.title)
}
let isExist = stateTitle.indexOf(title)
// === -1 则不存在
if (isExist == -1) {
//保存到vuex中暴露的公共资源store的state中
stateRouterMeunMessage.push(dict)
console.log('store', store.state.routerMeunMessage)
}
}
// 若tab项中的name与v-model绑定的值一致,则该tab被选中
let obj = {}
if(nowPath.name == 'sqlPage') {
this.nameFlag ? this.active_name = this.nameFlag : this.active_name = nowPath.meta.title
obj = {
'title': 'SQL查询',
'path': nowPath.path
}
} else {
this.active_name = nowPath.meta.title;
obj = {
'title': nowPath.meta.title,
'path': nowPath.path
}
}
sessionStorage.setItem('lastPath',JSON.stringify(obj))
},
// 深度观察监听
deep: true
},
},
created() {
bus.$on('collapse-content', (msg) => {
this.collapse = msg;
});
bus.$on('collapse', (msg) => {
this.collapse = msg;
bus.$emit('collapse-content', msg);
});
},
mounted() {
//刷新vuex共享值
store.state.routerMeunMessage = [{ title: '主页', path: '/index'}]
//刷新时或者重新进入页面时挂载主页与首页内容
const lastPage = JSON.parse(sessionStorage.getItem('lastPath'))
if(lastPage) {
if(lastPage.path!=='/index') {
store.state.routerMeunMessage .push(lastPage)
}
this.active_name = lastPage.title;
this.$router.push(lastPage.path)
}
console.log(store.state.routerMeunMessage);
this.init();
if (document.body.clientWidth > 767) {
this.collapseChage();
}
},
methods: {
init() {
this.$axios.run('/api/menu/menu', {
userCode: this.username
}, (res) => {
if (res.code === 0) {
this.$store.commit('menuListLoad', res.data);
// this.menuList = res.data;
} else {
this.$message.error(res.message);
}
});
},
routerIndex() {
this.$router.push('/index');
},
reload() {
this.isRouterAlive = false;
this.$nextTick(() => (this.isRouterAlive = true));
},
initCollapse() {
if (this.collapse === true) {
this.mainStyle.left = '65px';
} else {
this.mainStyle.left = '200px';
}
},
outLogin() {
this.$confirm('确定要退出吗?', '退出登录', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
sessionStorage.clear();
localStorage.clear();
this.$router.push({
path: '/'
});
});
},
// 用户名下拉菜单选择事件
handleCommand(command) {
if (command === 'loginout') {
sessionStorage.removeItem('ms_username');
this.$router.push('/login');
}
},
// 侧边栏折叠
collapseChage() {
this.collapse = ! this.collapse;
bus.$emit('collapse', this.collapse);
localStorage.setItem('collapse', JSON.stringify(this.collapse));
this.initCollapse();
},
// tab点击
tabClick(nowTabMessage) {
console.log('tab点击获取的数据:', nowTabMessage)
//获取点击的tab的name值
const name = nowTabMessage.name
this.nameFlag = nowTabMessage.name
//这里跳转路由是控制菜单的高亮,通过点击tab来获取tab的点击回调值,从而来改变路由,而菜单通过defult-active来控制选中的菜单项
const rens = store.state.routerMeunMessage;
let obj = {};
rens.map(i => {
if(i.title == name) {
//将当前路由赋值给 menuActive,这里用来控制激活菜单项
this.menuActive = i.path
this.$router.push(i.path)
if(i.path == '/reinsurance/staticPage/sqlPage'){
obj = {
'title': 'SQL查询',
'path': i.path
}
} else {
obj = {
'title': i.title,
'path': i.path
}
}
sessionStorage.setItem('lastPath',JSON.stringify(obj))
}
})
},
//删除tab标签
tabRemove(tabMessage) {
//获取到的回调值为路由,因为我赋予的tab-pane的name为path,tab的删除回调值为name的值
console.log('已删除tab', tabMessage);
//先判断tab列表是否只有一条信息
let routerMeunMessage = store.state.routerMeunMessage
console.log(routerMeunMessage);
if (routerMeunMessage.length == 1) {
this.active_name = routerMeunMessage[0].title
} else {
//获取点击了删除的tab项在store.state.routerMeunMessage(vuex数据共享) 列表中的位置
let routerMeunMessage = store.state.routerMeunMessage
const getTabIndex = routerMeunMessage.findIndex((item) => (item.title == tabMessage))
//routerMeunMessage数据长度,长度在删除前计算,否则会逻辑出错
const lengthMeunMessage = routerMeunMessage.length
//删除指定的tab对象信息,在页面的tab中会消失
routerMeunMessage.splice(getTabIndex, 1)
//删除的tab存在两种情况,一是后面还有tab标签项,一种是其是最后一个tab项
//若删除的为最后一个tab
if (getTabIndex + 1 == lengthMeunMessage) {
//高亮它的前一个tab项
this.active_name = routerMeunMessage[getTabIndex - 1].title
//菜单高亮也要对应,跳转路由使其defult-active获取最新的值来控制对应菜单项高亮,菜单的defult-active通过获取最新的路由来控制高亮
this.menuActive = routerMeunMessage[getTabIndex - 1].path
this.$router.push(routerMeunMessage[getTabIndex - 1].path)
}
//若删除的不是最后一个tab项
else {
//高亮它的后一个tab项,这里直接使用getTabIndex,因为删除后后一个tab顶替了它原本的位置index
console.log(getTabIndex);
console.log(routerMeunMessage[getTabIndex]);
this.active_name = routerMeunMessage[getTabIndex].title
//菜单高亮也要对应,跳转路由使其defult-active获取最新的值来控制对应菜单项高亮
this.menuActive = routerMeunMessage[getTabIndex].path
this.$router.push(routerMeunMessage[getTabIndex].path)
}
}
},
},
components: {
// curmbs
}
};
</script>
<style lang="less">
html {
background-color: #F4F8FB;
/* background-color: #333; */
}
.el-submenu>.el-submenu__title {
height: 50px ;
line-height: 50px ;
font-weight: bold;
font-family: "lucida Grande",Verdana,"Microsoft YaHei";
}
.el-submenu>.el-submenu__title{
font-size: 12px;
padding:0 10px!important;
}
.el-submenu>.el-submenu__title .el-submenu__icon-arrow{
-webkit-transform: rotateZ(-90deg);
-ms-transform: rotate(-90deg);
transform: rotateZ(-90deg);
font-size: 14px;
color: #666;
margin-top: -5.5px;
}
.el-submenu.is-opened>.el-submenu__title .el-submenu__icon-arrow{
-webkit-transform: rotateZ(0deg)!important;
-ms-transform: rotate(0deg)!important;
transform: rotateZ(0deg)!important;
}
.flex {
// display: flex;
width: 100%;
// flex-flow: row nowrap;
}
.update-pwd-dialog {
padding-left: 0 !important;
margin-top: 0 !important;
}
@media (max-width: 1310px) {
.update-pwd-dialog {
.el-form-item__label {
width: 110px !important;
font-size: 12px;
}
}
}
@media (max-width: 1000px) {
.update-pwd-dialog {
.el-input {
width: 140px !important;
}
}
}
.upd-dialog-group {
.el-dialog {
top: 10vh;
}
.el-dialog--center .el-dialog__body {
padding: 15px 25px 10px;
}
}
.dbCustomClass {
.el-dialog__header {
background: #fff;
}
.el-dialog__body {
background: #fff;
position: relative;
top: 0;
left: 0;
max-height: 60vh;
overflow: auto;
}
.el-dialog__footer {
background: #fff;
}
}
</style>
<style lang="less" scoped>
@import "../../assets/css/reset.css";
.el-menu {
border: 1px solid #DCDFE6;
-webkit-box-shadow: 0 2px 4px 0 rgba(0,0,0,.12), 0 0 6px 0 rgba(0,0,0,.04);
box-shadow: 0 2px 4px 0 rgba(0,0,0,.12), 0 0 6px 0 rgba(0,0,0,.04);
}
.el-menu::-webkit-scrollbar,
.el-menu-vertical-demo::-webkit-scrollbar{
display: none;
}
.el-menu-vertical-demo {
position: fixed;
left: 0;
top: 60px;
z-index: 998;
height: calc(100vh - 60px)!important;
overflow-y: auto;
overflow-x: hidden;
background-color: rgb(89, 61, 249);
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 192px;
// height: 100%;
position: fixed;
}
.el-menu-item{
height: 36px;
line-height: 36px;
color: #84D6BF;
min-width: 192px;
font-size: 12px;
padding-left:55px!important;
font-family: "lucida Grande",Verdana,"Microsoft YaHei";
// overflow: auto;
}
.el-menu-item::-webkit-scrollbar {
display: none;
}
.fa {
margin: 0 5px 2px 10px;
color: #666;
font-size: 18px;
}
.main-r {
height: 100%;
width: 100%;
box-sizing: border-box;
}
.nodeName {
margin-left: 15px;
}
.routerView::-webkit-scrollbar{
display: none;
}
.header {
position: fixed;
z-index: 999;
box-sizing: border-box;
height: 60px;
right: 0;
width: 100%;
font-size: 22px;
color: #f2f2f2;
background: @mainColor;
box-shadow: 3px 0px 5px 3px #ccc;
}
.collapse-btn {
float: left;
padding: 0 21px;
cursor: pointer;
line-height: 60px;
}
.logoReplace {
// border: 1px solid red;
width: 60px;
height: 38px;
position: absolute;
top: 4px;
left: 22px;
background: @mainColor;
}
.header .logo {
cursor: pointer;
float: left;
width: 148px;
height: 38px;
margin-top: 4px;
// border: 1px solid #000;
background: url(../../assets/images/logo.png) no-repeat;
}
.logo-title {
color: rgba(20, 17, 16, 0.986);
font-size: 26px;
font-weight: bold;
padding-right: 8px;
}
.header-right {
float: right;
padding-right: 50px;
}
.header-user-con {
display: flex;
height: 60px;
align-items: center;
}
.btn-bell {
position: relative;
width: 30px;
height: 30px;
text-align: center;
border-radius: 15px;
cursor: pointer;
}
.btn-bell-badge {
position: absolute;
right: 0;
top: -2px;
width: 8px;
height: 8px;
border-radius: 4px;
background: #f56c6c;
color: #f2f2f2;
}
.btn-bell .el-icon-bell {
color: #f2f2f2;
}
.user-name {
margin-left: 10px;
}
.user-avator {
margin-left: 20px;
}
.user-avator img {
display: block;
width: 40px;
height: 40px;
border-radius: 50%;
}
.el-dropdown-link {
color: #f2f2f2;
cursor: pointer;
}
.el-dropdown-menu__item {
text-align: center;
}
.mainUrl_box {
margin-right: 0px;
text-align: left;
height: 30px;
line-height: 30px;
border-bottom: 1px solid #e2e2e2;
}
::v-deep .el-tabs {
height: 30px;
margin: 0;
padding: 0;
}
::v-deep .el-tabs__nav-scroll{
height: 30px;
margin: 0;
padding: 0;
}
// tab被激活时改变的背景颜色,tab项被选中后会被追加.is-active,由v-model="active_name"绑定的值控制选中的tab项
::v-deep .el-tabs__item{
line-height: 30px;
height: 30px;
}
::v-deep .el-tabs--card>.el-tabs__header {
border-bottom: none !important;
}
</style>
sqlpage.vue
<template>
<div class="box">
<el-card>
<div class="box-titles">
<p>SQL查询</p>
</div>
<div class="box-form" style="border: 1px solid #2c9af9; border-radius: 12px">
<el-form :inline="true" :model="formData" class="sql-form">
<el-row :gutter="5">
<el-col :span="12">
<el-form-item>
<el-button type="success" icon="el-icon-search" @click="enforcement(1)">执行</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-share" @click="newWindow">新窗口</el-button>
</el-form-item>
<el-form-item>
<el-button icon="el-icon-upload" @click="derive">导出</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div class="box-textarea">
<codemirror ref="newCm" v-model="sqlTextArea" :options="cmOptions" >
</codemirror>
</div>
<el-row>
<el-pagination :total="total" :current-page.sync="pageNo" :page-size="pageSize" layout="total, prev, pager, next, jumper" @current-change="pageCurrentChange"></el-pagination>
</el-row>
<div class="box-sqltable">
<el-table ref="multipleTable" style="border-radius: 12px; height: 100%;" :data="querySqlMessage" border>
<el-table-column min-width="60" show-overflow-tooltip align="center" v-for="(item, index) in tableLabel" :key="index" :prop="item" :label="item"></el-table-column>
</el-table>
</div>
</el-card>
</div>
</template>
<script>
import store from '@/store';
import 'codemirror/mode/sql/sql.js'
// 主题css
import 'codemirror/theme/3024-day.css'
// require active-line.js
import 'codemirror/addon/selection/active-line.js'
// closebrackets
import 'codemirror/addon/edit/closebrackets.js'
import "codemirror/lib/codemirror.css";
import "codemirror/theme/idea.css";
import "codemirror/theme/panda-syntax.css";
import "codemirror/addon/hint/show-hint.css";
require("codemirror/lib/codemirror");
require("codemirror/mode/sql/sql");
require("codemirror/addon/hint/show-hint");
require("codemirror/addon/hint/sql-hint");
import Base64 from '../../utils/base';
export default {
name: 'sqlPage',
components: {
sqlTable: ()=> import('./components/sqltable')
},
data() {
return {
formData: {},
sqlTextArea: '',
pageNo: 1,
pageSize: 20,
total: 0,
cmOptions: {
lineNumbers: true,
mode: {
name: "text/x-plsql"
},
matchBrackets: true,
styleActiveLine: true, // 当前行背景高亮
indentWithTabs: true,
smartIndent: true,
lineNumbers: true,
spellcheck:true,
extraKeys: {"Ctrl": "autocomplete"}, //自动提示配置
hintOptions: {
completeSingle: false
},
lineWrapping: true, // 自动换行
theme: '3024-day' // 主题根据需要自行配置
},
times: 0,
editableTabsValue: "0",
editableTabs: [],
sqlArr: [],
sqlArrBase: [],
querySqlMessage: [],
tableLabel: []
};
},
mounted() {
},
methods: {
enforcement(val) {
if (!this.sqlTextArea) {
this.$message.warning('请输入要执行的SQL');
return;
}
if (this.sqlTextArea.indexOf(';') == -1) {
this.$message.warning('请输入英文半角";"分号结尾');
return;
}
// // 使用 RegExp 对象的构造函数替换所有\n后截取sql语句
this.sqlArr = [];
this.sqlArrBase = [];
this.sqlArr = this.sqlTextArea.replace(new RegExp('\n', 'g'),"").split(';');
this.sqlArr.pop();
this.sqlArr.map(item=> {
this.sqlArrBase.push(Base64.encode(item))
})
const params = {
operator: sessionStorage.getItem('uname') ? sessionStorage.getItem('uname') : '',
sql: this.sqlArrBase,
page: val,
size : 20
}
this.$axios.run('/api/v1/sql/execute', params , (res) => {
if (res.code == '1') {
if (res.data[0].fieldName !== null) {
this.querySqlMessage = res.data[0].dataList;
this.tableLabel= res.data[0].fieldName;
}
this.$message({
type: 'success',
message: res.msg
});
} else {
this.$message({
type: 'error',
message: res.msg
});
}
});
},
newWindow() {
console.log("打开新窗口");
let stateRouterMeunMessage = store.state.routerMeunMessage
let num = 0;
stateRouterMeunMessage.map(item => {
if(item.path == '/reinsurance/staticPage/sqlPage') {
num++
}
})
console.log(num);
const obj = {
title: 'SQL窗口' + num,
path: '/reinsurance/staticPage/sqlPage',
}
//保存到vuex中暴露的公共资源store的state中
stateRouterMeunMessage.push(obj)
// console.log('store', store.state.routerMeunMessage)
},
derive() {
this.$prompt('请输入文件名', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
customClass: 'persdsd',
}).then(({ value }) => {
this.$axios.run('/api/v1/file/export', {
operator: sessionStorage.getItem('uname') ? sessionStorage.getItem('uname') : '',
params: this.sqlTextArea,
filename: value
} , (res) => {
if (res.code == '0') {
this.$message({
type: 'success',
message: '导出成功,你的文件名为 ' + value
});
} else {
this.$message({
type: 'error',
message: res.message || res.message
});
}
});
}).catch(() => {
this.$message({
type: 'info',
message: '取消'
});
});
},
pageCurrentChange(val) {
this.pageNo = val;
this.enforcement(val);
},
renderHeader(h, data) {
return h("span", [
h(
"el-tooltip",
{
attrs: {
class: "item",
effect: "dark",
content: data.column.label,
placement: "top",
},
},
[h("span", data.column.label)]
),
]);
},
},
};
</script>
<style lang='less' scoped>
.sql-form {
// display: flex;
// padding-left: 5px;
// border-radius: 12px;
.el-form-item {
transform:translateY(35%);
margin-left: 20px;
}
// ::v-deep .el-form-item__label {
// width: 70px;
// }
// .el-row {
// width: 100%;
// }
}
.box-textarea {
// background-color: green;
margin: 5px auto;
height: 220px;
border: 1px solid #2c9af9;
border-radius:12px;
box-shadow: 2px 2px 5px #bbb;
}
::v-deep .CodeMirror {
width: 100%;
height: 220px;
border-radius:12px;
}
.box-sqltable {
height: 400px;
border:1px solid #2c9af9;
border-radius:12px;
box-shadow: 2px 2px 5px #bbb;
margin: 5px auto;
}
::v-deep .el-table__body-wrapper {
height: 100%;
}
::v-deep .el-table--mini td, .el-table--mini th {
padding: 0;
}
::v-deep .el-table thead {
color: #000000;
line-height: 15px;
}
::v-deep .el-table .cell {
line-height: 14px;
padding: 0;
// white-space: nowrap;
text-overflow: ellipsis;
white-space: nowrap;
}
::v-deep .el-tabs{
height: 100%;
.el-tabs__header {
padding: 0;
position: relative;
margin: 0 0 5px;
}
.el-tabs__nav-wrap {
height: 100%;
border-radius: 12px;
.el-tabs__item {
height: 100%;
line-height: 20px;
}
}
.el-tabs__content{
height: 82%;
.el-tab-pane {
height: 100%;
}
}
}
</style>
<style lang="less">
.persdsd {
.el-message-box__content {
text-align: center;
}
}
</style>
router.js