init
This commit is contained in:
58
src/layout/components/AppMain.vue
Normal file
58
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<keep-alive :include="cachedViews">
|
||||
<router-view :key="key" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppMain',
|
||||
computed: {
|
||||
cachedViews() {
|
||||
return this.$store.state.tagsView.cachedViews
|
||||
},
|
||||
key() {
|
||||
return this.$route.path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 50px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fixed-header+.app-main {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header+.app-main {
|
||||
padding-top: 84px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// fix css style bug in open el-dialog
|
||||
.el-popup-parent--hidden {
|
||||
.fixed-header {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
37
src/layout/components/Footerbar.vue
Normal file
37
src/layout/components/Footerbar.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<!--
|
||||
* @Author: your name
|
||||
* @Date: 2021-01-27 10:07:42
|
||||
* @LastEditTime: 2021-03-02 10:11:08
|
||||
* @LastEditors: gtz
|
||||
* @Description: In User Settings Edit
|
||||
* @FilePath: \mt-bus-fe\src\layout\components\FooterBar.vue
|
||||
-->
|
||||
<template>
|
||||
<div class="footerbar">
|
||||
© {{ 'copyright.company' | i18nFilter }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
computed: {},
|
||||
watch: {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.footerbar{
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-indent: 2em;
|
||||
background: #fff;
|
||||
z-index: 999;
|
||||
box-shadow: 2px -2px 2px rgba($color: #000000, $alpha: .1);
|
||||
}
|
||||
</style>
|
||||
316
src/layout/components/Navbar.vue
Normal file
316
src/layout/components/Navbar.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<hamburger v-if="showhome" id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||
|
||||
<div v-if="showTitle" style="font-size: 20px; float: left; font-weight: bold; padding-left: 20px; color: #606266">
|
||||
<img src="../../assets/img/cnbm.png" style="width: 1.6em; height: 1.6em; position: relative; top: .4em" alt="">
|
||||
{{ 'title' | i18nFilter }}
|
||||
</div>
|
||||
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
|
||||
|
||||
<div class="right-menu">
|
||||
|
||||
<div v-if="showhome" class="right-menu-back" @click="toHome">
|
||||
<svg-icon class="item-icon" icon-class="home" />
|
||||
{{ 'navbar.homepage' | i18nFilter }}
|
||||
</div>
|
||||
<div class="right-menu-back">
|
||||
<el-badge :value="alarmList.length" class="item" style="line-height: 0; margin: 0 10px;" @click.native="toAlarm">
|
||||
<svg-icon style="width: 24px; height: 24px" class="item-icon" icon-class="alarm" />
|
||||
</el-badge>
|
||||
</div>
|
||||
|
||||
<template>
|
||||
<lang-select class="right-menu-item hover-effect" />
|
||||
</template>
|
||||
<div v-if="showhome" class="right-menu-back">
|
||||
<a href="Lodap.zip"><i class="el-icon-download" /></a>
|
||||
</div>
|
||||
<div class="right-menu-back">
|
||||
<i class="el-icon-alarm-clock" />
|
||||
{{ formatTime }}
|
||||
</div>
|
||||
|
||||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
|
||||
<div class="avatar-wrapper">
|
||||
<img :src="require('@/assets/img/head.png')" class="user-avatar">
|
||||
<div class="avatar-username">{{ username }}</div>
|
||||
<!-- <i class="el-icon-caret-bottom" /> -->
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<router-link to="/profile/index">
|
||||
<el-dropdown-item>
|
||||
<svg-icon class="item-icon" icon-class="user" />
|
||||
{{ 'navbar.profile' | i18nFilter }}
|
||||
</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item @click.native="toHome">
|
||||
<svg-icon class="item-icon" icon-class="help" />
|
||||
{{ 'navbar.help' | i18nFilter }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="logout">
|
||||
<svg-icon class="item-icon" icon-class="logout" />
|
||||
{{ 'navbar.logOut' | i18nFilter }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<!-- <div class="right-menu-back" @click="toHome">
|
||||
<svg-icon class="item-icon" icon-class="help" />
|
||||
{{ 'navbar.help' | i18nFilter }}
|
||||
</div>
|
||||
|
||||
<div class="right-menu-back" @click="logout">
|
||||
<svg-icon class="item-icon" icon-class="logout" />
|
||||
{{ 'navbar.logOut' | i18nFilter }}
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { alarmInfoList } from '@/api/basicData/AlarmManagement/alarmInfo'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import LangSelect from '@/components/LangSelect'
|
||||
import moment from 'moment'
|
||||
|
||||
let logoutInterval = null
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Breadcrumb,
|
||||
Hamburger,
|
||||
LangSelect
|
||||
},
|
||||
props: {
|
||||
showhome: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return true
|
||||
}
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
logoutTime: 1800000,
|
||||
alarmList: [],
|
||||
blurTime: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar',
|
||||
'avatar',
|
||||
'username',
|
||||
'choicepart'
|
||||
]),
|
||||
formatTime() {
|
||||
return moment(this.logoutTime - 28800000).format('HH:mm:ss')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
logoutTime: function(val) {
|
||||
if (val <= 0) {
|
||||
this.logout()
|
||||
}
|
||||
},
|
||||
$route: function() {
|
||||
this.getAlarm()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getAlarm()
|
||||
logoutInterval = setInterval(() => {
|
||||
this.logoutTime -= 1000
|
||||
}, 1000)
|
||||
addEventListener('click', this.logoutTimeReset)
|
||||
addEventListener('keydown', this.logoutTimeReset)
|
||||
addEventListener('visibilitychange', this.visibilitychangeListener)
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(logoutInterval)
|
||||
removeEventListener('click', this.logoutTimeReset)
|
||||
removeEventListener('keydown', this.logoutTimeReset)
|
||||
removeEventListener('visibilitychange', this.visibilitychangeListener)
|
||||
},
|
||||
methods: {
|
||||
// 浏览器失去焦点
|
||||
winBlur() {
|
||||
this.blurTime = new Date()
|
||||
clearInterval(logoutInterval)
|
||||
},
|
||||
// 浏览器获取焦点
|
||||
winFocus() {
|
||||
if (this.logoutTime - (new Date() - this.blurTime).valueOf() <= 0) {
|
||||
this.logout()
|
||||
} else {
|
||||
this.logoutTime -= (new Date() - this.blurTime).valueOf()
|
||||
logoutInterval = setInterval(() => {
|
||||
this.logoutTime -= 1000
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
// 重置退出时间
|
||||
logoutTimeReset() {
|
||||
this.logoutTime = 1800000
|
||||
},
|
||||
visibilitychangeListener() {
|
||||
if (document.hidden) {
|
||||
this.winBlur()
|
||||
} else {
|
||||
this.winFocus()
|
||||
}
|
||||
},
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar')
|
||||
},
|
||||
async logout() {
|
||||
console.log("sessionStorage.getItem('loginspc')", sessionStorage.getItem('loginspc'))
|
||||
if (sessionStorage.getItem('loginspc') === 'login') {
|
||||
const spcUrl = `/spc/upms/userinfo/logoutUser?account=${'ZJCadmin'}`
|
||||
axios.get(spcUrl).then(res => {
|
||||
if (res.data.code !== 'OK') {
|
||||
this.$message.error(this.$t('module.quality.spc.error'))
|
||||
}
|
||||
sessionStorage.setItem('loginspc', 'logout')
|
||||
})
|
||||
}
|
||||
await this.$store.dispatch('user/logout')
|
||||
console.log('`/login?redirect=${this.$route.fullPath}`', `/login?redirect=${this.$route.fullPath}`)
|
||||
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
|
||||
clearInterval(logoutInterval)
|
||||
},
|
||||
async toHome() {
|
||||
this.$store.dispatch('app/setChoicepart')
|
||||
this.$router.push('/')
|
||||
},
|
||||
getAlarm() {
|
||||
alarmInfoList({
|
||||
current: 1,
|
||||
size: 1000,
|
||||
alarmType: '',
|
||||
code: '',
|
||||
alarmGrade: ''
|
||||
}).then(response => {
|
||||
if (response.data.records) {
|
||||
this.alarmList = response.data.records
|
||||
} else {
|
||||
this.alarmList = []
|
||||
}
|
||||
})
|
||||
},
|
||||
toAlarm() {
|
||||
this.$router.push('/AlarmManagement/AlarmInfo')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.errLog-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
float: right;
|
||||
height: 100%;
|
||||
line-height: 50px;
|
||||
position: relative;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.right-menu-back{
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
font-size: 14px;
|
||||
padding: 0 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: #5a5e66;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
&.hover-effect {
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 20px;
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.avatar-username {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -14px;
|
||||
}
|
||||
|
||||
.el-icon-caret-bottom {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 25px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
src/layout/components/Settings/index.vue
Normal file
108
src/layout/components/Settings/index.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="drawer-container">
|
||||
<div>
|
||||
<h3 class="drawer-title">{{ 'settings.title' | i18nFilter }}</h3>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>{{ 'settings.theme' | i18nFilter }}</span>
|
||||
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>{{ 'settings.tagsView' | i18nFilter }}</span>
|
||||
<el-switch v-model="tagsView" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>{{ 'settings.fixedHeader' | i18nFilter }}</span>
|
||||
<el-switch v-model="fixedHeader" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>{{ 'settings.sidebarLogo' | i18nFilter }}</span>
|
||||
<el-switch v-model="sidebarLogo" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ThemePicker from '@/components/ThemePicker'
|
||||
|
||||
export default {
|
||||
components: { ThemePicker },
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
fixedHeader: {
|
||||
get() {
|
||||
return this.$store.state.settings.fixedHeader
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'fixedHeader',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
tagsView: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsView
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsView',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
sidebarLogo: {
|
||||
get() {
|
||||
return this.$store.state.settings.sidebarLogo
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'sidebarLogo',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
themeChange(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'theme',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drawer-container {
|
||||
padding: 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
|
||||
.drawer-title {
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, .85);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.drawer-item {
|
||||
color: rgba(0, 0, 0, .65);
|
||||
font-size: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.drawer-switch {
|
||||
float: right
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
src/layout/components/Sidebar/FixiOSBug.js
Normal file
26
src/layout/components/Sidebar/FixiOSBug.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
computed: {
|
||||
device() {
|
||||
return this.$store.state.app.device
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
|
||||
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
|
||||
this.fixBugIniOS()
|
||||
},
|
||||
methods: {
|
||||
fixBugIniOS() {
|
||||
const $subMenu = this.$refs.subMenu
|
||||
if ($subMenu) {
|
||||
const handleMouseleave = $subMenu.handleMouseleave
|
||||
$subMenu.handleMouseleave = (e) => {
|
||||
if (this.device === 'mobile') {
|
||||
return
|
||||
}
|
||||
handleMouseleave(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/layout/components/Sidebar/Item.vue
Normal file
29
src/layout/components/Sidebar/Item.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'MenuItem',
|
||||
functional: true,
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
render(h, context) {
|
||||
const { icon, title } = context.props
|
||||
const vnodes = []
|
||||
|
||||
if (icon) {
|
||||
vnodes.push(<svg-icon icon-class={icon}/>)
|
||||
}
|
||||
|
||||
if (title) {
|
||||
vnodes.push(<span slot='title'>{(title)}</span>)
|
||||
}
|
||||
return vnodes
|
||||
}
|
||||
}
|
||||
</script>
|
||||
43
src/layout/components/Sidebar/Link.vue
Normal file
43
src/layout/components/Sidebar/Link.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps(to)">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isExternal() {
|
||||
return isExternal(this.to)
|
||||
},
|
||||
type() {
|
||||
if (this.isExternal) {
|
||||
return 'a'
|
||||
}
|
||||
return 'router-link'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkProps(to) {
|
||||
if (this.isExternal) {
|
||||
return {
|
||||
href: to,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
return {
|
||||
to: to
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
100
src/layout/components/Sidebar/Logo.vue
Normal file
100
src/layout/components/Sidebar/Logo.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<!--
|
||||
* @Author: gtz
|
||||
* @Date: 2021-01-27 10:07:42
|
||||
* @LastEditors: gtz
|
||||
* @LastEditTime: 2021-03-12 17:07:14
|
||||
* @Description: file content
|
||||
-->
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo">
|
||||
<h1 v-else :style="{fontSize}" class="sidebar-title">{{ title }} </h1>
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo">
|
||||
<h1 :style="{fontSize}" class="sidebar-title">{{ title }} </h1>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SidebarLogo',
|
||||
props: {
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
logo: require('../../../assets/img/cnbm.png')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-enter,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
background: #2b2f3a;
|
||||
padding-left: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
& .sidebar-logo-link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& .sidebar-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
line-height: 50px;
|
||||
font-size: 14px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
.sidebar-logo {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
src/layout/components/Sidebar/SidebarItem.vue
Normal file
136
src/layout/components/Sidebar/SidebarItem.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-tooltip class="item" effect="dark" :content="onlyOneChild.meta.title" placement="right">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
||||
<div class="menu-item-box">
|
||||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||
</div>
|
||||
</el-menu-item>
|
||||
</el-tooltip>
|
||||
</app-link>
|
||||
</template>
|
||||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
||||
|
||||
<template slot="title">
|
||||
<el-tooltip class="item" effect="dark" :content="item.meta.title" placement="right">
|
||||
<div class="tooltip-box">
|
||||
<div class="menu-item-box">
|
||||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
|
||||
</div>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<sidebar-item
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
class="nest-menu"
|
||||
/>
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import Item from './Item'
|
||||
import AppLink from './Link'
|
||||
import FixiOSBug from './FixiOSBug'
|
||||
|
||||
export default {
|
||||
name: 'SidebarItem',
|
||||
components: { Item, AppLink },
|
||||
mixins: [FixiOSBug],
|
||||
props: {
|
||||
// route object
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
|
||||
// TODO: refactor with render function
|
||||
this.onlyOneChild = null
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
itemChildren() {
|
||||
return this.item.children.filter(subRoute => !subRoute.meta.hidden)
|
||||
// return this.rawItem.children.filter(subRoute => !subRoute.hidden)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasOneShowingChild(children = [], parent) {
|
||||
const showingChildren = children.filter(item => {
|
||||
if (item.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
this.onlyOneChild = item
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
resolvePath(routePath) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(this.basePath)) {
|
||||
return this.basePath
|
||||
}
|
||||
return path.resolve(this.basePath, routePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-menu-item{
|
||||
.menu-item-box{
|
||||
width: 100%;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
}
|
||||
.el-submenu__title{
|
||||
.tooltip-box{
|
||||
position: relative;
|
||||
left: 20px;
|
||||
.menu-item-box{
|
||||
position: relative;
|
||||
right: 20px;
|
||||
width: calc(100% - 18px);
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/layout/components/Sidebar/index.vue
Normal file
98
src/layout/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<!--
|
||||
* @Author: your name
|
||||
* @Date: 2021-01-27 10:07:42
|
||||
* @LastEditTime: 2021-03-08 11:59:59
|
||||
* @LastEditors: gtz
|
||||
* @Description: In User Settings Edit
|
||||
* @FilePath: \mt-bus-fe\src\layout\components\Sidebar\index.vue
|
||||
-->
|
||||
<template>
|
||||
<div :class="{'has-logo':showLogo}">
|
||||
<logo v-if="showLogo" :font-size="language === 'en' ? '16px' : '12px'" :title="'title' | i18nFilter" :collapse="isCollapse" />
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:background-color="variables.menuBg"
|
||||
:text-color="variables.menuText"
|
||||
:unique-opened="false"
|
||||
:active-text-color="variables.menuActiveText"
|
||||
:collapse-transition="false"
|
||||
mode="vertical"
|
||||
>
|
||||
<sidebar-item v-for="route in routeList" :key="route.path" :item="route" :base-path="route.path" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Logo from './Logo'
|
||||
import SidebarItem from './SidebarItem'
|
||||
import variables from '@/styles/variables.scss'
|
||||
// import { constantRoutes } from '@/router'
|
||||
import store from '@/store'
|
||||
export default {
|
||||
components: { SidebarItem, Logo },
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar',
|
||||
'choicepart',
|
||||
'language'
|
||||
]),
|
||||
partList() {
|
||||
const cangoList = []
|
||||
const constantRoutes = store.getters.permission_routes
|
||||
constantRoutes.map(item => {
|
||||
if (!item.hidden && item.meta) {
|
||||
cangoList.push(item)
|
||||
}
|
||||
})
|
||||
const formatList = cangoList.map((item, index) => {
|
||||
return this.setIndex(item, index)
|
||||
})
|
||||
return formatList
|
||||
},
|
||||
routeList() {
|
||||
return [this.partList[this.choicepart]]
|
||||
},
|
||||
activeMenu() {
|
||||
const route = this.$route
|
||||
const { meta, path } = route
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
return path
|
||||
},
|
||||
showLogo() {
|
||||
return this.$store.state.settings.sidebarLogo
|
||||
},
|
||||
variables() {
|
||||
return variables
|
||||
},
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: function(val) {
|
||||
if (val.meta.routeIndex >= 0) {
|
||||
this.$store.dispatch('app/setChoicepart', val.meta.routeIndex)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setIndex(list, index) {
|
||||
list.meta.routeIndex = index
|
||||
if (list.children) {
|
||||
list.children.map(item => {
|
||||
this.setIndex(item, index)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
94
src/layout/components/TagsView/ScrollPane.vue
Normal file
94
src/layout/components/TagsView/ScrollPane.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
||||
|
||||
export default {
|
||||
name: 'ScrollPane',
|
||||
data() {
|
||||
return {
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
scrollWrapper() {
|
||||
return this.$refs.scrollContainer.$refs.wrap
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
|
||||
},
|
||||
methods: {
|
||||
handleScroll(e) {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||
},
|
||||
emitScroll() {
|
||||
this.$emit('scroll')
|
||||
},
|
||||
moveToTarget(currentTag) {
|
||||
const $container = this.$refs.scrollContainer.$el
|
||||
const $containerWidth = $container.offsetWidth
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
const tagList = this.$parent.$refs.tag
|
||||
|
||||
let firstTag = null
|
||||
let lastTag = null
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0]
|
||||
lastTag = tagList[tagList.length - 1]
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex = tagList.findIndex(item => item === currentTag)
|
||||
const prevTag = tagList[currentIndex - 1]
|
||||
const nextTag = tagList[currentIndex + 1]
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
||||
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
::v-deep {
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0px;
|
||||
}
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
294
src/layout/components/TagsView/index.vue
Normal file
294
src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div id="tags-view-container" class="tags-view-container">
|
||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
ref="tag"
|
||||
:key="tag.path"
|
||||
:class="isActive(tag)?'active':''"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
tag="span"
|
||||
class="tags-view-item"
|
||||
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
|
||||
>
|
||||
<!-- @contextmenu.prevent.native="openMenu(tag,$event)" 在上面一行 -->
|
||||
{{ tag.title }}
|
||||
<span class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)">{{ 'tagsView.refresh' | i18nFilter }}</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">{{ 'tagsView.close' | i18nFilter }}</li>
|
||||
<li @click="closeOthersTags">{{ 'tagsView.closeOthers' | i18nFilter }}</li>
|
||||
<li @click="closeAllTags(selectedTag)">{{ 'tagsView.closeAll' | i18nFilter }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollPane from './ScrollPane'
|
||||
import path from 'path'
|
||||
|
||||
export default {
|
||||
components: { ScrollPane },
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
selectedTag: {},
|
||||
affixTags: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visitedViews() {
|
||||
return this.$store.state.tagsView.visitedViews
|
||||
},
|
||||
routes() {
|
||||
// return this.$store.state.permission.routes
|
||||
return []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.addTags()
|
||||
this.moveToCurrentTag()
|
||||
},
|
||||
visible(value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTags()
|
||||
this.addTags()
|
||||
},
|
||||
methods: {
|
||||
isActive(route) {
|
||||
return route.path === this.$route.path
|
||||
},
|
||||
isAffix(tag) {
|
||||
return tag.meta && tag.meta.affix
|
||||
},
|
||||
filterAffixTags(routes, basePath = '/') {
|
||||
let tags = []
|
||||
routes.forEach(route => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path)
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta }
|
||||
})
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags = this.filterAffixTags(route.children, route.path)
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags]
|
||||
}
|
||||
}
|
||||
})
|
||||
return tags
|
||||
},
|
||||
initTags() {
|
||||
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
||||
console.log('affixTags', affixTags)
|
||||
for (const tag of affixTags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
this.$store.dispatch('tagsView/addVisitedView', tag)
|
||||
}
|
||||
}
|
||||
},
|
||||
addTags() {
|
||||
const { name } = this.$route
|
||||
if (name) {
|
||||
this.$store.dispatch('tagsView/addView', this.$route)
|
||||
}
|
||||
return false
|
||||
},
|
||||
moveToCurrentTag() {
|
||||
const tags = this.$refs.tag
|
||||
this.$nextTick(() => {
|
||||
for (const tag of tags) {
|
||||
if (tag.to.path === this.$route.path) {
|
||||
this.$refs.scrollPane.moveToTarget(tag)
|
||||
// when query is different then update
|
||||
if (tag.to.fullPath !== this.$route.fullPath) {
|
||||
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
refreshSelectedTag(view) {
|
||||
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
|
||||
const { fullPath } = view
|
||||
this.$nextTick(() => {
|
||||
this.$router.replace({
|
||||
path: '/redirect' + fullPath
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
closeSelectedTag(view) {
|
||||
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
|
||||
if (this.isActive(view)) {
|
||||
this.toLastView(visitedViews, view)
|
||||
}
|
||||
})
|
||||
},
|
||||
closeOthersTags() {
|
||||
this.$router.push(this.selectedTag)
|
||||
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
|
||||
this.moveToCurrentTag()
|
||||
})
|
||||
},
|
||||
closeAllTags(view) {
|
||||
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
|
||||
if (this.affixTags.some(tag => tag.path === view.path)) {
|
||||
return
|
||||
}
|
||||
this.toLastView(visitedViews, view)
|
||||
})
|
||||
},
|
||||
toLastView(visitedViews, view) {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
this.$router.push(latestView.fullPath)
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === 'Dashboard') {
|
||||
// to reload home page
|
||||
this.$router.replace({ path: '/redirect' + view.fullPath })
|
||||
} else {
|
||||
this.$router.push('/')
|
||||
}
|
||||
}
|
||||
},
|
||||
openMenu(tag, e) {
|
||||
const menuMinWidth = 105
|
||||
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
|
||||
const offsetWidth = this.$el.offsetWidth // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||
const left = e.clientX - offsetLeft + 15 // 15: margin right
|
||||
|
||||
if (left > maxLeft) {
|
||||
this.left = maxLeft
|
||||
} else {
|
||||
this.left = left
|
||||
}
|
||||
|
||||
this.top = e.clientY
|
||||
this.visible = true
|
||||
this.selectedTag = tag
|
||||
},
|
||||
closeMenu() {
|
||||
this.visible = false
|
||||
},
|
||||
handleScroll() {
|
||||
this.closeMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
border: 1px solid #d8dce5;
|
||||
color: #495060;
|
||||
background: #fff;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
color: #fff;
|
||||
border-color: #42b983;
|
||||
&::before {
|
||||
content: '';
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
//reset element css of el-icon-close
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
.el-icon-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: 2px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
||||
transform-origin: 100% 50%;
|
||||
&:before {
|
||||
transform: scale(.6);
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #b4bccc;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
src/layout/components/index.js
Normal file
13
src/layout/components/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* @Author: gtz
|
||||
* @Date: 2021-01-27 10:07:42
|
||||
* @LastEditors: gtz
|
||||
* @LastEditTime: 2021-02-24 14:42:30
|
||||
* @Description: file content
|
||||
*/
|
||||
export { default as AppMain } from './AppMain'
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as Footerbar } from './Footerbar'
|
||||
export { default as Settings } from './Settings'
|
||||
export { default as Sidebar } from './Sidebar/index.vue'
|
||||
export { default as TagsView } from './TagsView/index.vue'
|
||||
Reference in New Issue
Block a user