This commit is contained in:
zzc
2025-03-28 18:28:06 +08:00
commit 939c43f281
206 changed files with 30419 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
import permissions from './permissions'
const install = function (Vue) {
Vue.directive('permissions', permissions)
}
if (window.Vue) {
window['permissions'] = permissions
Vue.use(install)
}
permissions.install = install
export default permissions

View File

@@ -0,0 +1,13 @@
import store from '@/store'
export default {
inserted(element, binding) {
const { value } = binding
const permissions = store.getters['user/permissions']
if (value && value instanceof Array && value.length > 0) {
const hasPermission = permissions.some((role) => value.includes(role))
if (!hasPermission)
element.parentNode && element.parentNode.removeChild(element)
}
},
}

View File

@@ -0,0 +1,65 @@
<template>
<!-- <img
v-if="isExternal"
:src="styleExternalIcon"
class="svg-external-icon svg-icon"
v-on="$listeners"
/> -->
<!-- <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg> -->
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
name: 'VabColorfulIcon',
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#colorful-icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return this.iconClass
},
},
}
</script>
<style lang="scss" scoped>
.svg-icon {
width: 1em;
height: 1em;
overflow: hidden;
vertical-align: -0.15em;
fill: currentColor;
&:hover {
opacity: 0.8;
}
}
.svg-external-icon {
display: inline-block;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<div v-if="errorLogs.length > 0">
<el-badge
:value="errorLogs.length"
@click.native="dialogTableVisible = true"
>
<el-button type="danger">
<vab-icon :icon="['fas', 'bug']" />
</el-button>
</el-badge>
<el-dialog
:visible.sync="dialogTableVisible"
append-to-body
title="vue-admin-better异常捕获(温馨提示:错误必须解决)"
width="70%"
>
<el-table :data="errorLogs">
<el-table-column label="报错路由">
<template slot-scope="{ row }">
<a :href="row.url" target="_blank">
<el-tag type="success">{{ row.url }}</el-tag>
</a>
</template>
</el-table-column>
<el-table-column label="错误信息">
<template slot-scope="{ row }">
<el-tag type="danger">{{ decodeUnicode(row.err.message) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="错误详情" width="120">
<template slot-scope="scope">
<el-popover placement="top-start" trigger="hover">
<div style="color: red">
{{ scope.row.err.stack }}
</div>
<el-button slot="reference">查看</el-button>
</el-popover>
</template>
</el-table-column>
<el-table-column label="操作" width="380">
<template slot-scope="{ row }">
<a
v-for="(item, index) in searchList"
:key="index"
:href="item.url + decodeUnicode(row.err.message)"
target="_blank"
>
<el-button style="margin-left: 5px" type="primary">
<vab-icon :icon="['fas', 'search']" />
{{ item.title }}
</el-button>
</a>
</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogTableVisible = false"> </el-button>
<el-button icon="el-icon-delete" type="danger" @click="clearAll">
暂不显示
</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { abbreviation, title } from '@/config'
import { mapGetters } from 'vuex'
export default {
name: 'VabErrorLog',
data() {
return {
dialogTableVisible: false,
title: title,
abbreviation: abbreviation,
searchList: [
{
title: '百度搜索',
url: 'https://www.baidu.com/baidu?wd=',
},
{
title: '谷歌搜索',
url: 'https://www.google.com/search?q=',
},
{
title: 'Magi搜索',
url: 'https://magi.com/search?q=',
},
],
}
},
computed: {
...mapGetters({
errorLogs: 'errorLog/errorLogs',
}),
},
methods: {
clearAll() {
this.dialogTableVisible = false
this.$store.dispatch('errorLog/clearErrorLog')
},
decodeUnicode(str) {
str = str.replace(/\\/g, '%')
str = unescape(str)
str = str.replace(/%/g, '\\')
str = str.replace(/\\/g, '')
return str
},
},
}
</script>
<style lang="scss" scoped>
::v-deep {
.el-badge {
.el-button {
display: flex;
align-items: center;
justify-items: center;
height: 28px;
}
}
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<span :title="isFullscreen ? '退出全屏' : '进入全屏'">
<vab-icon
:icon="[
'fas',
isFullscreen ? 'compress-arrows-alt' : 'expand-arrows-alt',
]"
@click="click"
></vab-icon>
</span>
</template>
<script>
import screenfull from 'screenfull'
export default {
name: 'VabFullScreenBar',
data() {
return {
isFullscreen: false,
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.isEnabled) {
this.$baseMessage('开启全屏失败', 'error')
return false
}
screenfull.toggle()
this.$emit('refresh')
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.isEnabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.isEnabled) {
screenfull.off('change', this.change)
}
},
},
}
</script>

View File

@@ -0,0 +1,75 @@
<template>
<a
aria-label="View source on Github"
class="github-corner"
href="https://github.com/vue-admin-better"
target="_blank"
>
<svg
aria-hidden="true"
class="github-color"
height="80"
viewBox="0 0 250 250"
width="80"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path
class="octo-arm"
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px"
/>
<path
class="octo-body"
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
/>
</svg>
</a>
</template>
<script>
export default {
name: 'VabGithubCorner',
}
</script>
<style lang="scss" scoped>
.github-corner {
position: absolute;
top: 0;
right: 0;
z-index: $base-z-index - 3;
.octo-arm {
animation: octocat-wave 560ms ease-in-out infinite;
}
&:hover {
.octo-arm {
animation: octocat-wave 560ms ease-in-out infinite;
}
}
.github-color {
color: #fff;
fill: $base-color-blue;
}
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(100deg);
}
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<el-col :span="24">
<div class="bottom-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormBottomPanel',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@@ -0,0 +1,25 @@
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="left-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormLeftPanel',
props: {
span: {
type: Number,
default: 14,
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@@ -0,0 +1,25 @@
<template>
<el-col :lg="span" :md="24" :sm="24" :xl="span" :xs="24">
<div class="right-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormRightPanel',
props: {
span: {
type: Number,
default: 10,
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<el-col :span="24">
<div class="top-panel">
<slot></slot>
</div>
</el-col>
</template>
<script>
export default {
name: 'VabQueryFormTopPanel',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>

View File

@@ -0,0 +1,63 @@
<template>
<el-row :gutter="0" class="vab-query-form">
<slot></slot>
</el-row>
</template>
<script>
export default {
name: 'VabQueryForm',
props: {},
data() {
return {}
},
created() {},
mounted() {},
methods: {},
}
</script>
<style lang="scss" scoped>
@mixin panel {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
}
.vab-query-form {
margin-bottom: 10px;
::v-deep {
.top-panel {
@include panel;
}
.bottom-panel {
@include panel;
padding-top: 14px;
border-top: 1px solid #dcdfe6;
}
.left-panel {
@include panel;
> .el-button,
.el-form-item {
margin: 5px;
}
}
.right-panel {
@include panel;
justify-content: flex-end;
.el-form-item {
margin: 5px;
}
}
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div
v-if="isExternal"
:style="styleExternalIcon"
class="svg-external-icon image-icon"
v-on="$listeners"
/>
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
name: 'VabRemixIcon',
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#remix-icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
backgroundImage: `url(${this.iconClass})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '50px', // 控制大小
height: '50px',
borderRadius: '50%', // 变成圆形
overflow: 'hidden',
display: 'inline-block',
}
}
},
}
</script>
<style lang="scss" scoped>
.svg-icon {
width: 1.125em;
height: 1.125em;
overflow: hidden;
fill: currentColor;
&:hover {
opacity: 0.8;
}
}
.image-icon {
display: inline-block;
background-size: cover !important;
background-position: center !important;
}
.svg-external-icon {
display: inline-block;
background-color: currentColor;
mask-size: cover !important;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<el-menu-item :index="handlePath(routeChildren.path)" @click="handleLink">
<vab-icon
v-if="routeChildren.meta.icon"
:icon="['fas', routeChildren.meta.icon]"
class="vab-fas-icon"
/>
<span>{{ routeChildren.meta.title }}</span>
<el-tag
v-if="routeChildren.meta && routeChildren.meta.badge"
effect="dark"
type="danger"
>
{{ routeChildren.meta.badge }}
</el-tag>
</el-menu-item>
</template>
<script>
import { isExternal } from '@/utils/validate'
import path from 'path'
export default {
name: 'VabMenuItem',
props: {
routeChildren: {
type: Object,
default() {
return null
},
},
item: {
type: Object,
default() {
return null
},
},
fullPath: {
type: String,
default: '',
},
},
methods: {
handlePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.fullPath)) {
return this.fullPath
}
return path.resolve(this.fullPath, routePath)
},
handleLink() {
const routePath = this.routeChildren.path
const target = this.routeChildren.meta.target
if (target === '_blank') {
if (isExternal(routePath)) {
window.open(routePath)
} else if (isExternal(this.fullPath)) {
window.open(this.fullPath)
} else if (
this.$route.path !== path.resolve(this.fullPath, routePath)
) {
let routeData = this.$router.resolve(
path.resolve(this.fullPath, routePath)
)
window.open(routeData.href)
}
} else {
if (isExternal(routePath)) {
window.location.href = routePath
} else if (isExternal(this.fullPath)) {
window.location.href = this.fullPath
} else if (
this.$route.path !== path.resolve(this.fullPath, routePath)
) {
this.$router.push(path.resolve(this.fullPath, routePath))
}
}
},
},
}
</script>

View File

@@ -0,0 +1,108 @@
<template>
<component
:is="menuComponent"
v-if="!item.hidden"
:full-path="fullPath"
:item="item"
:route-children="routeChildren"
>
<template v-if="item.children && item.children.length">
<vab-side-bar-item
v-for="route in item.children"
:key="route.path"
:full-path="handlePath(route.path)"
:item="route"
/>
</template>
</component>
</template>
<script>
import { isExternal } from '@/utils/validate'
import path from 'path'
export default {
name: 'VabSideBarItem',
props: {
item: {
type: Object,
required: true,
},
fullPath: {
type: String,
default: '',
},
},
data() {
this.onlyOneChild = null
return {}
},
computed: {
menuComponent() {
if (
this.handleChildren(this.item.children, this.item) &&
(!this.routeChildren.children ||
this.routeChildren.notShowChildren) &&
!this.item.alwaysShow
) {
return 'VabMenuItem'
} else {
return 'VabSubmenu'
}
},
},
methods: {
handleChildren(children = [], parent) {
if (children === null) children = []
const showChildren = children.filter((item) => {
if (item.hidden) {
return false
} else {
this.routeChildren = item
return true
}
})
if (showChildren.length === 1) {
return true
}
if (showChildren.length === 0) {
this.routeChildren = {
...parent,
path: '',
notShowChildren: true,
}
return true
}
return false
},
handlePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.fullPath)) {
return this.fullPath
}
return path.resolve(this.fullPath, routePath)
},
},
}
</script>
<style lang="scss" scoped>
.vab-nav-icon {
margin-right: 4px;
}
::v-deep {
.el-tag {
float: right;
height: 16px;
padding-right: 4px;
padding-left: 4px;
margin-top: calc((#{$base-menu-item-height} - 16px) / 2);
line-height: 16px;
border: 0;
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<el-submenu
ref="subMenu"
:index="handlePath(item.path)"
:popper-append-to-body="false"
>
<template slot="title">
<vab-icon
v-if="item.meta && item.meta.icon"
:icon="['fas', item.meta.icon]"
class="vab-fas-icon"
/>
<vab-remix-icon
v-if="item.meta && item.meta.remixIcon"
:icon-class="item.meta.remixIcon"
class="vab-remix-icon"
/>
<span>{{ item.meta.title }}</span>
</template>
<slot />
</el-submenu>
</template>
<script>
import { isExternal } from '@/utils/validate'
import path from 'path'
export default {
name: 'VabSubmenu',
props: {
routeChildren: {
type: Object,
default() {
return null
},
},
item: {
type: Object,
default() {
return null
},
},
fullPath: {
type: String,
default: '',
},
},
methods: {
handlePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.fullPath)) {
return this.fullPath
}
return path.resolve(this.fullPath, routePath)
},
},
}
</script>

View File

@@ -0,0 +1,145 @@
<template>
<el-scrollbar :class="{ 'is-collapse': collapse }" class="side-bar-container">
<vab-logo />
<el-menu
:active-text-color="variables['menu-color-active']"
:background-color="variables['menu-background']"
:collapse="collapse"
:collapse-transition="false"
:default-active="activeMenu"
:default-openeds="defaultOpens"
:text-color="variables['menu-color']"
:unique-opened="uniqueOpened"
mode="vertical"
>
<template v-for="route in routes">
<vab-side-bar-item
:key="route.path"
:full-path="route.path"
:item="route"
/>
</template>
</el-menu>
</el-scrollbar>
</template>
<script>
import variables from '@/styles/variables.scss'
import { mapGetters } from 'vuex'
import { defaultOopeneds, uniqueOpened } from '@/config'
export default {
name: 'VabSideBar',
data() {
return {
uniqueOpened,
}
},
computed: {
...mapGetters({
collapse: 'settings/collapse',
routes: 'routes/routes',
}),
defaultOpens() {
if (this.collapse) {
}
return defaultOopeneds
},
activeMenu() {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
variables() {
return variables
},
},
}
</script>
<style lang="scss" scoped>
@mixin active {
&:hover {
color: $base-color-white;
background-color: $base-menu-background-active !important;
}
&.is-active {
color: $base-color-white;
background-color: $base-menu-background-active !important;
}
}
.side-bar-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: $base-z-index;
width: $base-left-menu-width;
height: 100vh;
overflow: hidden;
background: $base-menu-background;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
transition: width $base-transition-time;
&.is-collapse {
width: $base-left-menu-width-min;
border-right: 0;
::v-deep {
.el-menu {
transition: width $base-transition-time;
}
.el-menu--collapse {
border-right: 0;
.el-submenu__icon-arrow {
right: 10px;
margin-top: -3px;
}
.el-menu-item,
.el-submenu {
text-align: center;
}
}
}
}
::v-deep {
.el-scrollbar__wrap {
overflow-x: hidden;
}
.el-menu {
border: 0;
.vab-fas-icon {
padding-right: 3px;
font-size: $base-font-size-default;
display: inline-block;
width: 14px;
}
.vab-remix-icon {
padding-right: 3px;
font-size: $base-font-size-default + 2;
}
}
.el-menu-item,
.el-submenu__title {
height: $base-menu-item-height;
line-height: $base-menu-item-height;
vertical-align: middle;
}
.el-menu-item {
@include active;
}
}
}
</style>

View File

@@ -0,0 +1,293 @@
<template>
<div id="tabs-bar-container" class="tabs-bar-container">
<el-tabs
v-model="tabActive"
class="tabs-content"
type="card"
@tab-click="handleTabClick"
@tab-remove="handleTabRemove"
>
<el-tab-pane
v-for="item in visitedRoutes"
:key="item.path"
:closable="!isAffix(item)"
:label="item.meta.title"
:name="item.path"
></el-tab-pane>
</el-tabs>
<el-dropdown @command="handleCommand">
<span style="cursor: pointer">
更多操作
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown" class="tabs-more">
<el-dropdown-item command="closeOtherstabs">
<vab-icon :icon="['fas', 'times-circle']" />
关闭其他
</el-dropdown-item>
<el-dropdown-item command="closeLefttabs">
<vab-icon :icon="['fas', 'arrow-alt-circle-left']"></vab-icon>
关闭左侧
</el-dropdown-item>
<el-dropdown-item command="closeRighttabs">
<vab-icon :icon="['fas', 'arrow-alt-circle-right']"></vab-icon>
关闭右侧
</el-dropdown-item>
<el-dropdown-item command="closeAlltabs">
<vab-icon :icon="['fas', 'ban']"></vab-icon>
关闭全部
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
import path from 'path'
import { mapGetters } from 'vuex'
export default {
name: 'VabTabsBar',
data() {
return {
affixtabs: [],
tabActive: '',
}
},
computed: {
...mapGetters({
visitedRoutes: 'tabsBar/visitedRoutes',
routes: 'routes/routes',
}),
},
watch: {
$route: {
handler(route) {
this.inittabs()
this.addtabs()
let tabActive = ''
this.visitedRoutes.forEach((item, index) => {
if (item.path === this.$route.path) {
tabActive = item.path
}
})
this.tabActive = tabActive
},
immediate: true,
},
},
mounted() {
//console.log(this.visitedRoutes);
},
methods: {
async handleTabRemove(tabActive) {
let view
this.visitedRoutes.forEach((item, index) => {
if (tabActive == item.path) {
view = item
}
})
const { visitedRoutes } = await this.$store.dispatch(
'tabsBar/delRoute',
view
)
if (this.isActive(view)) {
this.toLastTag(visitedRoutes, view)
}
},
handleTabClick(tab) {
const route = this.visitedRoutes.filter((item, index) => {
if (tab.index == index) return item
})[0]
if (this.$route.path !== route.path) {
this.$router.push({
path: route.path,
query: route.query,
fullPath: route.fullPath,
})
} else {
return false
}
},
isActive(route) {
return route.path === this.$route.path
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
filterAffixtabs(routes, basePath = '/') {
let tabs = []
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tabs.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta },
})
}
if (route.children) {
const temptabs = this.filterAffixtabs(route.children, route.path)
if (temptabs.length >= 1) {
tabs = [...tabs, ...temptabs]
}
}
})
return tabs
},
inittabs() {
const affixtabs = (this.affixtabs = this.filterAffixtabs(this.routes))
for (const tag of affixtabs) {
if (tag.name) {
this.$store.dispatch('tabsBar/addVisitedRoute', tag)
}
}
},
addtabs() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tabsBar/addVisitedRoute', this.$route)
}
return false
},
handleCommand(command) {
switch (command) {
case 'refreshRoute':
this.refreshRoute()
break
case 'closeOtherstabs':
this.closeOtherstabs()
break
case 'closeLefttabs':
this.closeLefttabs()
break
case 'closeRighttabs':
this.closeRighttabs()
break
case 'closeAlltabs':
this.closeAlltabs()
break
}
},
async refreshRoute() {
this.$baseEventBus.$emit('reloadrouter-view')
},
async closeSelectedTag(view) {
const { visitedRoutes } = await this.$store.dispatch(
'tabsBar/delRoute',
view
)
if (this.isActive(view)) {
this.toLastTag(visitedRoutes, view)
}
},
async closeOtherstabs() {
const view = await this.toThisTag()
await this.$store.dispatch('tabsBar/delOthersRoutes', view)
},
async closeLefttabs() {
const view = await this.toThisTag()
await this.$store.dispatch('tabsBar/delLeftRoutes', view)
},
async closeRighttabs() {
const view = await this.toThisTag()
await this.$store.dispatch('tabsBar/delRightRoutes', view)
},
async closeAlltabs() {
const view = await this.toThisTag()
const { visitedRoutes } = await this.$store.dispatch(
'tabsBar/delAllRoutes'
)
if (this.affixtabs.some((tag) => tag.path === view.path)) {
return
}
this.toLastTag(visitedRoutes, view)
},
toLastTag(visitedRoutes, view) {
const latestView = visitedRoutes.slice(-1)[0]
if (latestView) {
this.$router.push(latestView)
} else {
this.$router.push('/')
}
},
async toThisTag() {
const view = this.visitedRoutes.filter((item, index) => {
if (item.path === this.$route.fullPath) {
return item
}
})[0]
if (this.$route.path !== view.path) this.$router.push(view)
return view
},
},
}
</script>
<style lang="scss" scoped>
.tabs-bar-container {
position: relative;
box-sizing: border-box;
display: flex;
align-content: center;
align-items: center;
justify-content: space-between;
height: $base-tabs-bar-height;
padding-right: $base-padding;
padding-left: $base-padding;
user-select: none;
background: $base-color-white;
border-top: 1px solid #f6f6f6;
::v-deep {
.fold-unfold {
margin-right: $base-padding;
}
}
.tabs-content {
width: calc(100% - 90px);
height: $base-tag-item-height;
::v-deep {
.el-tabs__nav-next,
.el-tabs__nav-prev {
height: $base-tag-item-height;
line-height: $base-tag-item-height;
}
.el-tabs__header {
border-bottom: 0;
.el-tabs__nav {
border: 0;
}
.el-tabs__item {
box-sizing: border-box;
height: $base-tag-item-height;
margin-right: 5px;
line-height: $base-tag-item-height;
border: 1px solid $base-border-color;
border-radius: $base-border-radius;
transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
&.is-active {
border: 1px solid $base-color-blue;
}
}
}
}
}
.more {
display: flex;
align-content: center;
align-items: center;
cursor: pointer;
}
}
</style>

228
layouts/VabTopBar/index.vue Normal file
View File

@@ -0,0 +1,228 @@
<template>
<div class="top-bar-container">
<div class="vab-main">
<el-row>
<el-col :lg="7" :md="7" :sm="7" :xl="7" :xs="7">
<vab-logo />
</el-col>
<el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="12">
<el-menu
:active-text-color="variables['menu-color-active']"
:background-color="variables['menu-background']"
:default-active="activeMenu"
:text-color="variables['menu-color']"
menu-trigger="hover"
mode="horizontal"
>
<template v-for="route in routes">
<vab-side-bar-item
v-if="!route.hidden"
:key="route.path"
:full-path="route.path"
:item="route"
/>
</template>
</el-menu>
</el-col>
<el-col :lg="5" :md="5" :sm="5" :xl="5" :xs="5">
<div class="right-panel">
<vab-error-log />
<vab-full-screen-bar @refresh="refreshRoute" />
<vab-theme-bar class="hidden-md-and-down" />
<vab-icon
:icon="['fas', 'redo']"
:pulse="pulse"
title="重载路由"
@click="refreshRoute"
/>
<vab-avatar />
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import variables from '@/styles/variables.scss'
import { mapGetters } from 'vuex'
export default {
name: 'VabTopBar',
data() {
return {
pulse: false,
menuTrigger: 'hover',
}
},
computed: {
...mapGetters({
routes: 'routes/routes',
visitedRoutes: 'tabsBar/visitedRoutes',
}),
activeMenu() {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
variables() {
return variables
},
},
methods: {
async refreshRoute() {
this.$baseEventBus.$emit('reload-router-view')
this.pulse = true
this.timeOutID = setTimeout(() => {
this.pulse = false
}, 1000)
},
},
beforeDestroy() {
clearTimeout(this.timeOutID);
}
};
</script>
<style lang="scss" scoped>
.top-bar-container {
display: flex;
align-items: center;
justify-items: flex-end;
height: $base-top-bar-height;
background: $base-menu-background;
.vab-main {
background: $base-menu-background;
::v-deep {
.el-menu {
&.el-menu--horizontal {
display: flex;
align-items: center;
justify-content: flex-end;
height: $base-top-bar-height;
border-bottom: 0 solid transparent !important;
.el-menu-item,
.el-submenu__title {
padding: 0 15px;
}
@media only screen and (max-width: 767px) {
.el-menu-item,
.el-submenu__title {
padding: 0 8px;
}
li:nth-child(4),
li:nth-child(5) {
display: none !important;
}
}
> .el-menu-item {
height: $base-top-bar-height;
line-height: $base-top-bar-height;
}
> .el-submenu {
.el-submenu__title {
height: $base-top-bar-height;
line-height: $base-top-bar-height;
}
}
}
svg {
width: 1rem;
margin-right: 3px;
}
&--horizontal {
.el-menu {
.el-menu-item,
.el-submenu__title {
height: $base-menu-item-height;
line-height: $base-menu-item-height;
}
}
.el-submenu,
.el-menu-item {
&.is-active {
background-color: $base-color-blue !important;
border-bottom: 0 solid transparent !important;
.el-submenu__title {
border-bottom: 0 solid transparent !important;
}
}
}
> .el-menu-item {
.el-tag {
margin-top: calc(#{$base-top-bar-height} / 2 - 7.5px);
margin-left: 5px;
}
@media only screen and (max-width: 1199px) {
.el-tag {
display: none;
}
}
&.is-active {
background-color: transparent !important;
border-bottom: 3px solid $base-color-blue !important;
}
}
}
}
}
}
.right-panel {
display: flex;
align-items: center;
justify-content: flex-end;
height: $base-top-bar-height;
::v-deep {
.user-name {
color: rgba($base-color-white, 0.9);
}
.user-name + i {
color: rgba($base-color-white, 0.9);
}
svg {
width: 1em;
height: 1em;
margin-right: 15px;
font-size: $base-font-size-big;
color: rgba($base-color-white, 0.9);
cursor: pointer;
fill: rgba($base-color-white, 0.9);
}
button {
svg {
margin-right: 0;
color: rgba($base-color-white, 0.9);
cursor: pointer;
fill: rgba($base-color-white, 0.9);
}
}
.el-badge {
margin-right: 15px;
}
}
}
}
</style>

9
layouts/index.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
webpackBarName: '',
webpackBanner: '',
donationConsole() {
const chalk = require('chalk')
console.log(chalk.green('项目启动成功'))
console.log('\n')
},
}

5
layouts/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "layouts",
"version": "1.0.0",
"main": "index.js"
}

View File

@@ -0,0 +1,16 @@
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'es5',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'always',
htmlWhitespaceSensitivity: 'ignore',
vueIndentScriptAndStyle: true,
endOfLine: 'lf',
}