first commit

This commit is contained in:
仲么了
2023-12-21 16:42:39 +08:00
commit 0f7b59f55b
79 changed files with 7638 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<template>
<el-aside v-if="!navTabs.state.tabFullScreen" :class="'layout-aside-' + config.layout.layoutMode + ' ' + (config.layout.shrink ? 'shrink' : '')">
<Logo v-if="config.layout.menuShowTopBar" />
<MenuVerticalChildren v-if="config.layout.layoutMode == 'Double'" />
<MenuVertical v-else />
</el-aside>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import Logo from '@/layouts/admin/components/logo.vue'
import MenuVertical from '@/layouts/admin/components/menus/menuVertical.vue'
import MenuVerticalChildren from '@/layouts/admin/components/menus/menuVerticalChildren.vue'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
defineOptions({
name: 'layout/aside',
})
const config = useConfig()
const navTabs = useNavTabs()
const menuWidth = computed(() => config.menuWidth())
</script>
<style scoped lang="scss">
.layout-aside-Default {
background: var(--ba-bg-color-overlay);
margin: 16px 0 16px 16px;
height: calc(100vh - 32px);
box-shadow: var(--el-box-shadow-light);
border-radius: var(--el-border-radius-base);
overflow: hidden;
transition: width 0.3s ease;
width: v-bind(menuWidth);
}
.layout-aside-Classic,
.layout-aside-Double {
background: var(--ba-bg-color-overlay);
margin: 0;
height: 100vh;
overflow: hidden;
transition: width 0.3s ease;
width: v-bind(menuWidth);
}
.shrink {
position: fixed;
top: 0;
left: 0;
z-index: 9999999;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div title="$'layouts.Exit full screen'" @mouseover.stop="onMouseover" @mouseout.stop="onMouseout">
<div @click.stop="onCloseFullScreen" class="close-full-screen" :style="{ top: state.closeBoxTop + 'px' }">
<Icon name="el-icon-Close" />
</div>
<div class="close-full-screen-on"></div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { useNavTabs } from '@/stores/navTabs'
const navTabs = useNavTabs()
const state = reactive({
closeBoxTop: 20,
})
onMounted(() => {
setTimeout(() => {
state.closeBoxTop = -30
}, 300)
})
/*
* 鼠标滑到顶部显示关闭全屏按钮
* 要检查 hover 的元素在外部直接使用事件而不是css
*/
const onMouseover = () => {
state.closeBoxTop = 20
}
const onMouseout = () => {
state.closeBoxTop = -30
}
const onCloseFullScreen = () => {
navTabs.setFullScreen(false)
}
</script>
<style scoped lang="scss">
.close-full-screen {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
right: calc(50% - 20px);
z-index: 9999999;
height: 40px;
width: 40px;
background-color: rgba($color: #000000, $alpha: 0.1);
border-radius: 50%;
box-shadow: var(--el-box-shadow-light);
transition: all 0.3s ease;
.icon {
color: rgba($color: #000000, $alpha: 0.6) !important;
}
&:hover {
background-color: rgba($color: #000000, $alpha: 0.3);
.icon {
color: rgba($color: #ffffff, $alpha: 0.6) !important;
}
}
}
.close-full-screen-on {
position: fixed;
top: 0;
z-index: 9999998;
height: 60px;
width: 100px;
left: calc(50% - 50px);
}
</style>

View File

@@ -0,0 +1,417 @@
<template>
<div class="layout-config-drawer">
<el-drawer
:model-value="configStore.layout.showDrawer"
title="布局配置"
size="310px"
@close="onCloseDrawer"
>
<el-scrollbar class="layout-mode-style-scrollbar">
<el-form ref="formRef" :model="configStore.layout">
<div class="layout-mode-styles-box">
<el-divider border-style="dashed">全局</el-divider>
<div class="layout-mode-box-style">
<el-row class="layout-mode-box-style-row" :gutter="10">
<el-col :span="12">
<div
@click="setLayoutMode('Default')"
class="layout-mode-style default"
:class="configStore.layout.layoutMode == 'Default' ? 'active' : ''"
>
<div class="layout-mode-style-box">
<div class="layout-mode-style-aside"></div>
<div class="layout-mode-style-container-box">
<div class="layout-mode-style-header"></div>
<div class="layout-mode-style-container"></div>
</div>
</div>
<div class="layout-mode-style-name">默认</div>
</div>
</el-col>
<el-col :span="12">
<div
@click="setLayoutMode('Classic')"
class="layout-mode-style classic"
:class="configStore.layout.layoutMode == 'Classic' ? 'active' : ''"
>
<div class="layout-mode-style-box">
<div class="layout-mode-style-aside"></div>
<div class="layout-mode-style-container-box">
<div class="layout-mode-style-header"></div>
<div class="layout-mode-style-container"></div>
</div>
</div>
<div class="layout-mode-style-name">经典</div>
</div>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="12">
<div
@click="setLayoutMode('Streamline')"
class="layout-mode-style streamline"
:class="configStore.layout.layoutMode == 'Streamline' ? 'active' : ''"
>
<div class="layout-mode-style-box">
<div class="layout-mode-style-container-box">
<div class="layout-mode-style-header"></div>
<div class="layout-mode-style-container"></div>
</div>
</div>
<div class="layout-mode-style-name">单栏</div>
</div>
</el-col>
<el-col :span="12">
<div
@click="setLayoutMode('Double')"
class="layout-mode-style double"
:class="configStore.layout.layoutMode == 'Double' ? 'active' : ''"
>
<div class="layout-mode-style-box">
<div class="layout-mode-style-aside"></div>
<div class="layout-mode-style-container-box">
<div class="layout-mode-style-header"></div>
<div class="layout-mode-style-container"></div>
</div>
</div>
<div class="layout-mode-style-name">双栏</div>
</div>
</el-col>
</el-row>
</div>
<el-divider border-style="dashed">全局</el-divider>
<div class="layout-config-global">
<el-form-item label="'后台页面切换动画">
<el-select
@change="onCommitState($event, 'mainAnimation')"
:model-value="configStore.layout.mainAnimation"
:placeholder="'layouts.Please select an animation name'"
>
<el-option label="slide-right" value="slide-right"></el-option>
<el-option label="slide-left" value="slide-left"></el-option>
<el-option label="el-fade-in-linear" value="el-fade-in-linear"></el-option>
<el-option label="el-fade-in" value="el-fade-in"></el-option>
<el-option label="el-zoom-in-center" value="el-zoom-in-center"></el-option>
<el-option label="el-zoom-in-top" value="el-zoom-in-top"></el-option>
<el-option label="el-zoom-in-bottom" value="el-zoom-in-bottom"></el-option>
</el-select>
</el-form-item>
</div>
<el-divider border-style="dashed">侧边栏</el-divider>
<div class="layout-config-aside">
<el-form-item label="侧边菜单栏背景色">
<el-color-picker
@change="onCommitColorState($event, 'menuBackground')"
:model-value="configStore.getColorVal('menuBackground')"
/>
</el-form-item>
<el-form-item label="侧边菜单文字颜色">
<el-color-picker
@change="onCommitColorState($event, 'menuColor')"
:model-value="configStore.getColorVal('menuColor')"
/>
</el-form-item>
<el-form-item label="侧边菜单激活项背景色">
<el-color-picker
@change="onCommitColorState($event, 'menuActiveBackground')"
:model-value="configStore.getColorVal('menuActiveBackground')"
/>
</el-form-item>
<el-form-item label="侧边菜单激活项文字色">
<el-color-picker
@change="onCommitColorState($event, 'menuActiveColor')"
:model-value="configStore.getColorVal('menuActiveColor')"
/>
</el-form-item>
<el-form-item label="显示侧边菜单顶栏(LOGO栏)">
<el-switch
@change="onCommitState($event, 'menuShowTopBar')"
:model-value="configStore.layout.menuShowTopBar"
></el-switch>
</el-form-item>
<el-form-item label="侧边菜单顶栏背景色">
<el-color-picker
@change="onCommitColorState($event, 'menuTopBarBackground')"
:model-value="configStore.getColorVal('menuTopBarBackground')"
/>
</el-form-item>
<el-form-item label="侧边菜单宽度(展开时)">
<el-input
@input="onCommitState($event, 'menuWidth')"
type="number"
:step="10"
:model-value="configStore.layout.menuWidth"
>
<template #append>px</template>
</el-input>
</el-form-item>
<el-form-item label="侧边菜单默认图标">
<IconSelector
@change="onCommitMenuDefaultIcon($event, 'menuDefaultIcon')"
:model-value="configStore.layout.menuDefaultIcon"
/>
</el-form-item>
<el-form-item label="侧边菜单水平折叠">
<el-switch
@change="onCommitState($event, 'menuCollapse')"
:model-value="configStore.layout.menuCollapse"
></el-switch>
</el-form-item>
<el-form-item label="侧边菜单手风琴">
<el-switch
@change="onCommitState($event, 'menuUniqueOpened')"
:model-value="configStore.layout.menuUniqueOpened"
></el-switch>
</el-form-item>
</div>
<el-divider border-style="dashed">顶栏</el-divider>
<div class="layout-config-aside">
<el-form-item label="顶栏背景色">
<el-color-picker
@change="onCommitColorState($event, 'headerBarBackground')"
:model-value="configStore.getColorVal('headerBarBackground')"
/>
</el-form-item>
<el-form-item label="顶栏文字色">
<el-color-picker
@change="onCommitColorState($event, 'headerBarTabColor')"
:model-value="configStore.getColorVal('headerBarTabColor')"
/>
</el-form-item>
<el-form-item label="顶栏悬停时背景色">
<el-color-picker
@change="onCommitColorState($event, 'headerBarHoverBackground')"
:model-value="configStore.getColorVal('headerBarHoverBackground')"
/>
</el-form-item>
<el-form-item label="顶栏菜单激活项背景色">
<el-color-picker
@change="onCommitColorState($event, 'headerBarTabActiveBackground')"
:model-value="configStore.getColorVal('headerBarTabActiveBackground')"
/>
</el-form-item>
<el-form-item label="顶栏菜单激活项文字色">
<el-color-picker
@change="onCommitColorState($event, 'headerBarTabActiveColor')"
:model-value="configStore.getColorVal('headerBarTabActiveColor')"
/>
</el-form-item>
</div>
<el-popconfirm
@confirm="restoreDefault"
:title="
'layouts.Are you sure you want to restore all configurations to the default values?'
"
>
<template #reference>
<div class="ba-center">
<el-button class="w80" type="info">恢复默认</el-button>
</div>
</template>
</el-popconfirm>
</div>
</el-form>
</el-scrollbar>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
import { useRouter } from 'vue-router'
import IconSelector from '@/components/baInput/components/iconSelector.vue'
import { STORE_CONFIG } from '@/stores/constant/cacheKey'
import { Local, Session } from '@/utils/storage'
import type { Layout } from '@/stores/interface'
const configStore = useConfig()
const navTabs = useNavTabs()
const router = useRouter()
const onCommitState = (value: any, name: any) => {
configStore.setLayout(name, value)
}
const onCommitColorState = (value: string | null, name: keyof Layout) => {
if (value === null) return
const colors = configStore.layout[name] as string[]
if (configStore.layout.isDark) {
colors[1] = value
} else {
colors[0] = value
}
configStore.setLayout(name, colors)
}
const setLayoutMode = (mode: string) => {
configStore.setLayoutMode(mode)
}
// 修改默认菜单图标
const onCommitMenuDefaultIcon = (value: any, name: any) => {
configStore.setLayout(name, value)
const menus = navTabs.state.tabsViewRoutes
navTabs.setTabsViewRoutes([])
setTimeout(() => {
navTabs.setTabsViewRoutes(menus)
}, 200)
}
const onCloseDrawer = () => {
configStore.setLayout('showDrawer', false)
}
const restoreDefault = () => {
Local.remove(STORE_CONFIG)
router.go(0)
}
</script>
<style scoped lang="scss">
.layout-config-drawer :deep(.el-input__inner) {
padding: 0 0 0 6px;
}
.layout-config-drawer :deep(.el-input-group__append) {
padding: 0 10px;
}
.layout-config-drawer :deep(.el-drawer__header) {
margin-bottom: 0 !important;
}
.layout-config-drawer :deep(.el-drawer__body) {
padding: 0;
}
.layout-mode-styles-box {
padding: 20px;
}
.layout-mode-box-style-row {
margin-bottom: 15px;
}
.layout-mode-style {
position: relative;
height: 100px;
border: 1px solid var(--el-border-color-light);
border-radius: var(--el-border-radius-small);
&:hover,
&.active {
border: 1px solid var(--el-color-primary);
}
.layout-mode-style-name {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-color-primary-light-5);
border-radius: 50%;
height: 50px;
width: 50px;
border: 1px solid var(--el-color-primary-light-3);
}
.layout-mode-style-box {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
&.default {
display: flex;
align-items: center;
justify-content: center;
.layout-mode-style-aside {
width: 18%;
height: 90%;
background-color: var(--el-border-color-lighter);
}
.layout-mode-style-container-box {
width: 68%;
height: 90%;
margin-left: 4%;
.layout-mode-style-header {
width: 100%;
height: 10%;
background-color: var(--el-border-color-lighter);
}
.layout-mode-style-container {
width: 100%;
height: 85%;
background-color: var(--el-border-color-extra-light);
margin-top: 5%;
}
}
}
&.classic {
display: flex;
align-items: center;
justify-content: center;
.layout-mode-style-aside {
width: 18%;
height: 100%;
background-color: var(--el-border-color-lighter);
}
.layout-mode-style-container-box {
width: 82%;
height: 100%;
.layout-mode-style-header {
width: 100%;
height: 10%;
background-color: var(--el-border-color);
}
.layout-mode-style-container {
width: 100%;
height: 90%;
background-color: var(--el-border-color-extra-light);
}
}
}
&.streamline {
display: flex;
align-items: center;
justify-content: center;
.layout-mode-style-container-box {
width: 100%;
height: 100%;
.layout-mode-style-header {
width: 100%;
height: 10%;
background-color: var(--el-border-color);
}
.layout-mode-style-container {
width: 100%;
height: 90%;
background-color: var(--el-border-color-extra-light);
}
}
}
&.double {
display: flex;
align-items: center;
justify-content: center;
.layout-mode-style-aside {
width: 18%;
height: 100%;
background-color: var(--el-border-color);
}
.layout-mode-style-container-box {
width: 82%;
height: 100%;
.layout-mode-style-header {
width: 100%;
height: 10%;
background-color: var(--el-border-color);
}
.layout-mode-style-container {
width: 100%;
height: 90%;
background-color: var(--el-border-color-extra-light);
}
}
}
}
.w80 {
width: 90%;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<el-header v-if="!navTabs.state.tabFullScreen" class="layout-header">
<component :is="config.layout.layoutMode + 'NavBar'"></component>
</el-header>
</template>
<script setup lang="ts">
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
import DefaultNavBar from '@/layouts/admin/components/navBar/default.vue'
import ClassicNavBar from '@/layouts/admin/components/navBar/classic.vue'
import StreamlineNavBar from '@/layouts/admin/components/menus/menuHorizontal.vue'
import DoubleNavBar from '@/layouts/admin/components/navBar/double.vue'
defineOptions({
name: 'layout/header',
components: { DefaultNavBar, ClassicNavBar, StreamlineNavBar, DoubleNavBar },
})
const config = useConfig()
const navTabs = useNavTabs()
</script>
<style scoped lang="scss">
.layout-header {
height: auto;
padding: 0;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="layout-logo">
<img v-if="!config.layout.menuCollapse" class="logo-img" src="@/assets/vue.svg" alt="logo" />
<div
v-if="!config.layout.menuCollapse"
:style="{ color: config.getColorVal('menuActiveColor') }"
class="website-name"
>
灿能
</div>
<Icon
v-if="config.layout.layoutMode != 'Streamline'"
@click="onMenuCollapse"
:name="config.layout.menuCollapse ? 'fa fa-indent' : 'fa fa-dedent'"
:class="config.layout.menuCollapse ? 'unfold' : ''"
:color="config.getColorVal('menuActiveColor')"
size="18"
class="fold"
/>
</div>
</template>
<script setup lang="ts">
import { useConfig } from '@/stores/config'
import { closeShade } from '@/utils/pageShade'
import { Session } from '@/utils/storage'
import { setNavTabsWidth } from '@/utils/layout'
const config = useConfig()
const onMenuCollapse = function () {
if (config.layout.shrink && !config.layout.menuCollapse) {
closeShade()
}
config.setLayout('menuCollapse', !config.layout.menuCollapse)
// 等待侧边栏动画结束后重新计算导航栏宽度
setTimeout(() => {
setNavTabsWidth()
}, 350)
}
</script>
<style scoped lang="scss">
.layout-logo {
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 10px;
background: v-bind(
'config.layout.layoutMode != "Streamline" ? config.getColorVal("menuTopBarBackground"):"transparent"'
);
}
.logo-img {
width: 28px;
}
.website-name {
display: block;
width: 180px;
padding-left: 4px;
font-size: var(--el-font-size-extra-large);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fold {
margin-left: auto;
}
.unfold {
margin: 0 auto;
}
</style>

View File

@@ -0,0 +1,18 @@
import type { RouteRecordRaw } from 'vue-router'
/**
* 寻找当前路由在顶栏菜单中的数据
*/
export const currentRouteTopActivity = (path: string, menus: RouteRecordRaw[]): RouteRecordRaw | false => {
for (let i = 0; i < menus.length; i++) {
const item: RouteRecordRaw = menus[i]
// 找到目标
if (item.path == path) return item
// 从子级继续寻找
if (item.children && item.children.length > 0) {
const find = currentRouteTopActivity(path, item.children)
if (find) return item
}
}
return false
}

View File

@@ -0,0 +1,105 @@
<template>
<div class="layouts-menu-horizontal">
<div class="menu-horizontal-logo" v-if="config.layout.menuShowTopBar">
<Logo />
</div>
<el-scrollbar ref="horizontalMenusRef" class="horizontal-menus-scrollbar">
<el-menu class="menu-horizontal" mode="horizontal" :default-active="state.defaultActive" :key="state.menuKey">
<MenuTree :extends="{ position: 'horizontal', level: 1 }" :menus="menus" />
</el-menu>
</el-scrollbar>
<NavMenus />
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import Logo from '@/layouts/admin/components/logo.vue'
import MenuTree from '@/layouts/admin/components/menus/menuTree.vue'
import { useRoute, onBeforeRouteUpdate, type RouteLocationNormalizedLoaded } from 'vue-router'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
import type { ScrollbarInstance } from 'element-plus'
import NavMenus from '@/layouts/admin/components/navMenus.vue'
import { uuid } from '@/utils/random'
const horizontalMenusRef = ref<ScrollbarInstance>()
const config = useConfig()
const navTabs = useNavTabs()
const route = useRoute()
const state = reactive({
menuKey: uuid(),
defaultActive: '',
})
const menus = computed(() => {
state.menuKey = uuid() // eslint-disable-line
return navTabs.state.tabsViewRoutes
})
// 激活当前路由的菜单
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
state.defaultActive = currentRoute.path
}
// 滚动条滚动到激活菜单所在位置
const verticalMenusScroll = () => {
nextTick(() => {
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.menu-horizontal li.is-active')
if (!activeMenu) return false
horizontalMenusRef.value?.setScrollTop(activeMenu.offsetTop)
})
}
onMounted(() => {
currentRouteActive(route)
verticalMenusScroll()
})
onBeforeRouteUpdate((to) => {
currentRouteActive(to)
})
</script>
<style scoped lang="scss">
.layouts-menu-horizontal {
display: flex;
align-items: center;
width: 100vw;
height: 60px;
background-color: var(--ba-bg-color-overlay);
border-bottom: solid 1px var(--el-color-info-light-8);
}
.menu-horizontal-logo {
width: 180px;
&:hover {
background-color: v-bind('config.getColorVal("headerBarHoverBackground")');
}
}
.horizontal-menus-scrollbar {
flex: 1;
}
.menu-horizontal {
border: none;
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
}
.el-sub-menu .icon,
.el-menu-item .icon {
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.is-active .icon {
color: var(--el-menu-active-color) !important;
}
.el-menu-item.is-active {
background-color: v-bind('config.getColorVal("menuActiveBackground")');
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<template v-for="menu in props.menus">
<template v-if="menu.children && menu.children.length > 0">
<el-sub-menu @click="onClickSubMenu(menu)" :index="menu.path" :key="menu.path">
<template #title>
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
<span>{{ menu.meta?.title ? menu.meta?.title : 'noTitle' }}</span>
</template>
<menu-tree :extends="{ ...props.extends, level: props.extends.level + 1 }" :menus="menu.children"></menu-tree>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="menu.path" :key="menu.path" @click="onClickMenu(menu)">
<Icon :color="config.getColorVal('menuColor')" :name="menu.meta?.icon ? menu.meta?.icon : config.layout.menuDefaultIcon" />
<span>{{ menu.meta?.title ? menu.meta?.title : 'noTitle' }}</span>
</el-menu-item>
</template>
</template>
</template>
<script setup lang="ts">
import { useConfig } from '@/stores/config'
import type { RouteRecordRaw } from 'vue-router'
import { getFirstRoute, onClickMenu } from '@/utils/router'
import { ElNotification } from 'element-plus'
const config = useConfig()
interface Props {
menus: RouteRecordRaw[]
extends?: {
level: number
[key: string]: any
}
}
const props = withDefaults(defineProps<Props>(), {
menus: () => [],
extends: () => {
return {
level: 1,
}
},
})
/**
* sub-menu-item 被点击 - 用于单栏布局和双栏布局
* 顶栏菜单:点击时打开第一个菜单
* 侧边菜单(若有):点击只展开收缩
*
* sub-menu-item 被点击时,也会触发到 menu-item 的点击事件,由 el-menu 内部触发,无法很好的排除,在此检查 level 值
*/
const onClickSubMenu = (menu: RouteRecordRaw) => {
if (props.extends?.position == 'horizontal' && props.extends.level <= 1 && menu.children?.length) {
const firstRoute = getFirstRoute(menu.children)
if (firstRoute) {
onClickMenu(firstRoute)
} else {
ElNotification({
type: 'error',
message: 'utils.No child menu to jump to!',
})
}
}
}
</script>
<style scoped lang="scss">
.el-sub-menu .icon,
.el-menu-item .icon {
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.is-active > .icon {
color: var(--el-menu-active-color) !important;
}
.el-menu-item.is-active {
background-color: v-bind('config.getColorVal("menuActiveBackground")');
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<el-scrollbar ref="verticalMenusRef" class="vertical-menus-scrollbar">
<el-menu
class="layouts-menu-vertical"
:collapse-transition="false"
:unique-opened="config.layout.menuUniqueOpened"
:default-active="state.defaultActive"
:collapse="config.layout.menuCollapse"
>
<MenuTree :menus="navTabs.state.tabsViewRoutes" />
</el-menu>
</el-scrollbar>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import MenuTree from '@/layouts/admin/components/menus/menuTree.vue'
import { useRoute, onBeforeRouteUpdate, type RouteLocationNormalizedLoaded } from 'vue-router'
import type { ScrollbarInstance } from 'element-plus'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
const config = useConfig()
const navTabs = useNavTabs()
const route = useRoute()
const verticalMenusRef = ref<ScrollbarInstance>()
const state = reactive({
defaultActive: '',
})
const verticalMenusScrollbarHeight = computed(() => {
let menuTopBarHeight = 0
if (config.layout.menuShowTopBar) {
menuTopBarHeight = 50
}
if (config.layout.layoutMode == 'Default') {
return 'calc(100vh - ' + (32 + menuTopBarHeight) + 'px)'
} else {
return 'calc(100vh - ' + menuTopBarHeight + 'px)'
}
})
// 激活当前路由的菜单
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
state.defaultActive = currentRoute.path
}
// 滚动条滚动到激活菜单所在位置
const verticalMenusScroll = () => {
nextTick(() => {
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.layouts-menu-vertical li.is-active')
if (!activeMenu) return false
verticalMenusRef.value?.setScrollTop(activeMenu.offsetTop)
})
}
onMounted(() => {
currentRouteActive(route)
verticalMenusScroll()
})
onBeforeRouteUpdate((to) => {
currentRouteActive(to)
})
</script>
<style>
.vertical-menus-scrollbar {
height: v-bind(verticalMenusScrollbarHeight);
background-color: v-bind('config.getColorVal("menuBackground")');
}
.layouts-menu-vertical {
border: 0;
padding-bottom: 30px;
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<el-scrollbar ref="verticalMenusRef" class="children-vertical-menus-scrollbar">
<el-menu
class="layouts-menu-vertical-children"
:collapse-transition="false"
:unique-opened="config.layout.menuUniqueOpened"
:default-active="state.defaultActive"
:collapse="config.layout.menuCollapse"
>
<MenuTree v-if="state.routeChildren.length > 0" :menus="state.routeChildren" />
</el-menu>
</el-scrollbar>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import MenuTree from '@/layouts/admin/components/menus/menuTree.vue'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import type { ScrollbarInstance } from 'element-plus'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
import { currentRouteTopActivity } from '@/layouts/admin/components/menus/helper'
const config = useConfig()
const navTabs = useNavTabs()
const route = useRoute()
const verticalMenusRef = ref<ScrollbarInstance>()
const state: {
defaultActive: string
routeChildren: RouteRecordRaw[]
} = reactive({
defaultActive: '',
routeChildren: [],
})
const verticalMenusScrollbarHeight = computed(() => {
let menuTopBarHeight = 0
if (config.layout.menuShowTopBar) {
menuTopBarHeight = 50
}
if (config.layout.layoutMode == 'Default') {
return 'calc(100vh - ' + (32 + menuTopBarHeight) + 'px)'
} else {
return 'calc(100vh - ' + menuTopBarHeight + 'px)'
}
})
/**
* 激活当前路由的菜单
* @param currentRoute 当前路由
*/
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
let routeChildren = currentRouteTopActivity(currentRoute.path, navTabs.state.tabsViewRoutes)
if (routeChildren) {
state.defaultActive = currentRoute.path
if (routeChildren.children && routeChildren.children.length > 0) {
state.routeChildren = routeChildren.children
} else {
state.routeChildren = [routeChildren]
}
} else if (!state.routeChildren) {
state.routeChildren = navTabs.state.tabsViewRoutes
}
}
/**
* 侧栏菜单滚动条滚动到激活菜单所在位置
*/
const verticalMenusScroll = () => {
nextTick(() => {
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.layouts-menu-vertical-children li.is-active')
if (!activeMenu) return false
verticalMenusRef.value?.setScrollTop(activeMenu.offsetTop)
})
}
onMounted(() => {
currentRouteActive(route)
verticalMenusScroll()
})
onBeforeRouteUpdate((to) => {
currentRouteActive(to)
})
</script>
<style>
.children-vertical-menus-scrollbar {
height: v-bind(verticalMenusScrollbarHeight);
background-color: v-bind('config.getColorVal("menuBackground")');
}
.layouts-menu-vertical-children {
border: 0;
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="nav-bar">
<div v-if="config.layout.shrink && config.layout.menuCollapse" class="unfold">
<Icon @click="onMenuCollapse" name="fa fa-indent" :color="config.getColorVal('menuActiveColor')" size="18" />
</div>
<NavTabs v-if="!config.layout.shrink" />
<NavMenus />
</div>
</template>
<script setup lang="ts">
import { useConfig } from '@/stores/config'
import NavTabs from '@/layouts/admin/components/navBar/tabs.vue'
import NavMenus from '../navMenus.vue'
import { showShade } from '@/utils/pageShade'
const config = useConfig()
const onMenuCollapse = () => {
showShade('ba-aside-menu-shade', () => {
config.setLayout('menuCollapse', true)
})
config.setLayout('menuCollapse', false)
}
</script>
<style scoped lang="scss">
.nav-bar {
display: flex;
height: 50px;
width: 100%;
background-color: v-bind('config.getColorVal("headerBarBackground")');
:deep(.nav-tabs) {
display: flex;
height: 100%;
position: relative;
.ba-nav-tab {
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
cursor: pointer;
z-index: 1;
height: 100%;
user-select: none;
color: v-bind('config.getColorVal("headerBarTabColor")');
transition: all 0.2s;
-webkit-transition: all 0.2s;
.close-icon {
padding: 2px;
margin: 2px 0 0 4px;
}
.close-icon:hover {
background: var(--ba-color-primary-light);
color: var(--el-border-color) !important;
border-radius: 50%;
}
&.active {
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
}
&:hover {
background-color: v-bind('config.getColorVal("headerBarHoverBackground")');
}
}
.nav-tabs-active-box {
position: absolute;
height: 50px;
background-color: v-bind('config.getColorVal("headerBarTabActiveBackground")');
transition: all 0.2s;
-webkit-transition: all 0.2s;
}
}
}
.unfold {
align-self: center;
padding-left: var(--ba-main-space);
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="nav-bar">
<NavTabs />
<NavMenus />
</div>
</template>
<script setup lang="ts">
import { useConfig } from '@/stores/config'
import NavTabs from '@/layouts/admin/components/navBar/tabs.vue'
import NavMenus from '../navMenus.vue'
const config = useConfig()
</script>
<style lang="scss" scoped>
.nav-bar {
display: flex;
height: 50px;
margin: 20px var(--ba-main-space) 0 var(--ba-main-space);
:deep(.nav-tabs) {
display: flex;
height: 100%;
position: relative;
.ba-nav-tab {
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
cursor: pointer;
z-index: 1;
user-select: none;
opacity: 0.7;
color: v-bind('config.getColorVal("headerBarTabColor")');
.close-icon {
padding: 2px;
margin: 2px 0 0 4px;
}
.close-icon:hover {
background: var(--ba-color-primary-light);
color: var(--el-border-color) !important;
border-radius: 50%;
}
&.active {
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
}
&:hover {
opacity: 1;
}
}
.nav-tabs-active-box {
position: absolute;
height: 40px;
border-radius: var(--el-border-radius-base);
background-color: v-bind('config.getColorVal("headerBarTabActiveBackground")');
box-shadow: var(--el-box-shadow-light);
transition: all 0.2s;
-webkit-transition: all 0.2s;
}
}
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="layouts-menu-horizontal-double">
<el-scrollbar ref="horizontalMenusRef" class="double-menus-scrollbar">
<el-menu class="menu-horizontal" mode="horizontal" :default-active="state.defaultActive" :key="state.menuKey">
<MenuTree :extends="{ position: 'horizontal', level: 1 }" :menus="menus" />
</el-menu>
</el-scrollbar>
<NavMenus />
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useRoute, onBeforeRouteUpdate, type RouteLocationNormalizedLoaded } from 'vue-router'
import { currentRouteTopActivity } from '@/layouts/admin/components/menus/helper'
import MenuTree from '@/layouts/admin/components/menus/menuTree.vue'
import NavMenus from '@/layouts/admin/components/navMenus.vue'
import type { ScrollbarInstance } from 'element-plus'
import { useNavTabs } from '@/stores/navTabs'
import { useConfig } from '@/stores/config'
import { uuid } from '@/utils/random'
const horizontalMenusRef = ref<ScrollbarInstance>()
const config = useConfig()
const navTabs = useNavTabs()
const route = useRoute()
const state = reactive({
menuKey: uuid(),
defaultActive: '',
})
const menus = computed(() => {
state.menuKey = uuid() // eslint-disable-line
return navTabs.state.tabsViewRoutes
})
// 激活当前路由的菜单
const currentRouteActive = (currentRoute: RouteLocationNormalizedLoaded) => {
let routeChildren = currentRouteTopActivity(currentRoute.path, navTabs.state.tabsViewRoutes)
if (routeChildren) state.defaultActive = currentRoute.path
}
// 滚动条滚动到激活菜单所在位置
const verticalMenusScroll = () => {
nextTick(() => {
let activeMenu: HTMLElement | null = document.querySelector('.el-menu.menu-horizontal li.is-active')
if (!activeMenu) return false
horizontalMenusRef.value?.setScrollTop(activeMenu.offsetTop)
})
}
onMounted(() => {
currentRouteActive(route)
verticalMenusScroll()
})
onBeforeRouteUpdate((to) => {
currentRouteActive(to)
})
</script>
<style scoped lang="scss">
.layouts-menu-horizontal-double {
display: flex;
align-items: center;
height: 60px;
background-color: var(--ba-bg-color-overlay);
border-bottom: solid 1px var(--el-color-info-light-8);
}
.double-menus-scrollbar {
width: 70vw;
}
.menu-horizontal {
border: none;
--el-menu-bg-color: v-bind('config.getColorVal("menuBackground")');
--el-menu-text-color: v-bind('config.getColorVal("menuColor")');
--el-menu-active-color: v-bind('config.getColorVal("menuActiveColor")');
}
.el-sub-menu .icon,
.el-menu-item .icon {
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.is-active .icon {
color: var(--el-menu-active-color) !important;
}
.el-menu-item.is-active {
background-color: v-bind('config.getColorVal("menuActiveBackground")');
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<div class="nav-tabs" ref="tabScrollbarRef">
<div
v-for="(item, idx) in navTabs.state.tabsView"
@click="onTab(item)"
@contextmenu.prevent="onContextmenu(item, $event)"
class="ba-nav-tab"
:class="navTabs.state.activeIndex == idx ? 'active' : ''"
:ref="tabsRefs.set"
:key="idx"
>
{{ item.meta.title }}
<transition @after-leave="selectNavTab(tabsRefs[navTabs.state.activeIndex])" name="el-fade-in">
<Icon
v-show="navTabs.state.tabsView.length > 1"
class="close-icon"
@click.stop="closeTab(item)"
size="15"
name="el-icon-Close"
/>
</transition>
</div>
<div :style="activeBoxStyle" class="nav-tabs-active-box"></div>
</div>
<Contextmenu ref="contextmenuRef" :items="state.contextmenuItems" @contextmenuItemClick="onContextmenuItem" />
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
import { useTemplateRefsList } from '@vueuse/core'
import type { ContextMenuItem, ContextmenuItemClickEmitArg } from '@/components/contextmenu/interface'
import useCurrentInstance from '@/utils/useCurrentInstance'
import Contextmenu from '@/components/contextmenu/index.vue'
import horizontalScroll from '@/utils/horizontalScroll'
import { getFirstRoute, routePush } from '@/utils/router'
import { adminBaseRoutePath } from '@/router/static'
const route = useRoute()
const router = useRouter()
const config = useConfig()
const navTabs = useNavTabs()
const { proxy } = useCurrentInstance()
const tabScrollbarRef = ref()
const tabsRefs = useTemplateRefsList<HTMLDivElement>()
const contextmenuRef = ref()
const state: {
contextmenuItems: ContextMenuItem[]
} = reactive({
contextmenuItems: [
{ name: 'refresh', label: '重新加载', icon: 'fa fa-refresh' },
{ name: 'close', label: '关闭标签', icon: 'fa fa-times' },
{ name: 'fullScreen', label: '当前标签全屏', icon: 'el-icon-FullScreen' },
{ name: 'closeOther', label: '关闭其他标签', icon: 'fa fa-minus' },
{ name: 'closeAll', label: '关闭全部标签', icon: 'fa fa-stop' }
]
})
const activeBoxStyle = reactive({
width: '0',
transform: 'translateX(0px)'
})
const onTab = (menu: RouteLocationNormalized) => {
router.push(menu)
}
const onContextmenu = (menu: RouteLocationNormalized, el: MouseEvent) => {
// 禁用刷新
state.contextmenuItems[0].disabled = route.path !== menu.path
// 禁用关闭其他和关闭全部
state.contextmenuItems[4].disabled = state.contextmenuItems[3].disabled =
navTabs.state.tabsView.length == 1 ? true : false
const { clientX, clientY } = el
contextmenuRef.value.onShowContextmenu(menu, {
x: clientX,
y: clientY
})
}
// tab 激活状态切换
const selectNavTab = function (dom: HTMLDivElement) {
if (!dom) {
return false
}
activeBoxStyle.width = dom.clientWidth + 'px'
activeBoxStyle.transform = `translateX(${dom.offsetLeft}px)`
let scrollLeft = dom.offsetLeft + dom.clientWidth - tabScrollbarRef.value.clientWidth
if (dom.offsetLeft < tabScrollbarRef.value.scrollLeft) {
tabScrollbarRef.value.scrollTo(dom.offsetLeft, 0)
} else if (scrollLeft > tabScrollbarRef.value.scrollLeft) {
tabScrollbarRef.value.scrollTo(scrollLeft, 0)
}
}
const toLastTab = () => {
const lastTab = navTabs.state.tabsView.slice(-1)[0]
if (lastTab) {
router.push(lastTab)
} else {
router.push(adminBaseRoutePath)
}
}
const closeTab = (route: RouteLocationNormalized) => {
navTabs.closeTab(route)
proxy.eventBus.emit('onTabViewClose', route)
if (navTabs.state.activeRoute?.path === route.path) {
toLastTab()
} else {
navTabs.setActiveRoute(navTabs.state.activeRoute!)
nextTick(() => {
selectNavTab(tabsRefs.value[navTabs.state.activeIndex])
})
}
contextmenuRef.value.onHideContextmenu()
}
const closeOtherTab = (menu: RouteLocationNormalized) => {
navTabs.closeTabs(menu)
navTabs.setActiveRoute(menu)
if (navTabs.state.activeRoute?.path !== route.path) {
router.push(menu!.path)
}
}
const closeAllTab = (menu: RouteLocationNormalized) => {
let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
if (firstRoute && firstRoute.path == menu.path) {
return closeOtherTab(menu)
}
if (firstRoute && firstRoute.path == navTabs.state.activeRoute?.path) {
return closeOtherTab(navTabs.state.activeRoute)
}
navTabs.closeTabs(false)
if (firstRoute) routePush(firstRoute.path)
}
const onContextmenuItem = async (item: ContextmenuItemClickEmitArg) => {
const { name, menu } = item
if (!menu) return
switch (name) {
case 'refresh':
proxy.eventBus.emit('onTabViewRefresh', menu)
break
case 'close':
closeTab(menu)
break
case 'closeOther':
closeOtherTab(menu)
break
case 'closeAll':
closeAllTab(menu)
break
case 'fullScreen':
if (route.path !== menu?.path) {
router.push(menu?.path as string)
}
navTabs.setFullScreen(true)
break
}
}
const updateTab = function (newRoute: RouteLocationNormalized) {
// 添加tab
navTabs.addTab(newRoute)
// 激活当前tab
navTabs.setActiveRoute(newRoute)
nextTick(() => {
selectNavTab(tabsRefs.value[navTabs.state.activeIndex])
})
}
onBeforeRouteUpdate(async to => {
updateTab(to)
})
onMounted(() => {
updateTab(router.currentRoute.value)
new horizontalScroll(tabScrollbarRef.value)
})
</script>
<style scoped lang="scss">
.dark {
.close-icon {
color: v-bind('config.getColorVal("headerBarTabColor")') !important;
}
.ba-nav-tab.active {
.close-icon {
color: v-bind('config.getColorVal("headerBarTabActiveColor")') !important;
}
}
}
.nav-tabs {
overflow-x: auto;
overflow-y: hidden;
margin-right: var(--ba-main-space);
scrollbar-width: none;
&::-webkit-scrollbar {
height: 5px;
}
&::-webkit-scrollbar-thumb {
background: #eaeaea;
border-radius: var(--el-border-radius-base);
box-shadow: none;
-webkit-box-shadow: none;
}
&::-webkit-scrollbar-track {
background: v-bind('config.layout.layoutMode == "Default" ? "none":config.getColorVal("headerBarBackground")');
}
&:hover {
&::-webkit-scrollbar-thumb:hover {
background: #c8c9cc;
}
}
}
.ba-nav-tab {
white-space: nowrap;
height: 40px;
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<div class="nav-menus" :class="configStore.layout.layoutMode">
<router-link class="h100" target="_blank" title="'Home'" to="/">
<div class="nav-menu-item">
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
name="el-icon-Monitor"
size="18"
/>
</div>
</router-link>
<div @click="onFullScreen" class="nav-menu-item" :class="state.isFullScreen ? 'hover' : ''">
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
v-if="state.isFullScreen"
name="local-full-screen-cancel"
size="18"
/>
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
v-else
name="el-icon-FullScreen"
size="18"
/>
</div>
<el-popover
@show="onCurrentNavMenu(true, 'adminInfo')"
@hide="onCurrentNavMenu(false, 'adminInfo')"
placement="bottom-end"
:hide-after="0"
:width="260"
trigger="click"
popper-class="admin-info-box"
v-model:visible="state.showAdminInfoPopover"
>
<template #reference>
<div class="admin-info" :class="state.currentNavMenu == 'adminInfo' ? 'hover' : ''">
<el-avatar :size="25" fit="fill">
<img :src="fullUrl(adminInfo.avatar)" alt="" />
</el-avatar>
<div class="admin-name">{{ adminInfo.nickname }}</div>
</div>
</template>
<div>
<div class="admin-info-base">
<el-avatar :size="70" fit="fill">
<img :src="fullUrl(adminInfo.avatar)" alt="" />
</el-avatar>
<div class="admin-info-other">
<div class="admin-info-name">{{ adminInfo.nickname }}</div>
<div class="admin-info-lasttime">{{ adminInfo.last_login_time }}</div>
</div>
</div>
<div class="admin-info-footer">
<el-button @click="onAdminInfo" type="primary" plain>{{ 'layouts.personal data' }}</el-button>
<el-button @click="onLogout" type="danger" plain>{{ 'layouts.cancellation' }}</el-button>
</div>
</div>
</el-popover>
<div @click="configStore.setLayout('showDrawer', true)" class="nav-menu-item">
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
name="fa fa-cogs"
size="18"
/>
</div>
<Config />
<!-- <TerminalVue /> -->
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
import screenfull from 'screenfull'
import { useConfig } from '@/stores/config'
import { ElMessage } from 'element-plus'
import Config from './config.vue'
import { useAdminInfo } from '@/stores/adminInfo'
import { Local, Session } from '@/utils/storage'
import { ADMIN_INFO } from '@/stores/constant/cacheKey'
import router from '@/router'
import { routePush } from '@/utils/router'
import { fullUrl } from '@/utils/common'
const adminInfo = useAdminInfo()
const configStore = useConfig()
const state = reactive({
isFullScreen: false,
currentNavMenu: '',
showLayoutDrawer: false,
showAdminInfoPopover: false
})
const onCurrentNavMenu = (status: boolean, name: string) => {
state.currentNavMenu = status ? name : ''
}
const onFullScreen = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('layouts.Full screen is not supported')
return false
}
screenfull.toggle()
screenfull.onchange(() => {
state.isFullScreen = screenfull.isFullscreen
})
}
const onAdminInfo = () => {
state.showAdminInfoPopover = false
routePush({ name: 'routine/adminInfo' })
}
const onLogout = () => {}
// const onClearCache = (type: string) => {
// if (type == 'storage' || type == 'all') {
// const adminInfo = Local.get(ADMIN_INFO)
// Session.clear()
// Local.clear()
// Local.set(ADMIN_INFO, adminInfo)
// if (type == 'storage') return
// }
// }
</script>
<style scoped lang="scss">
.nav-menus.Default {
border-radius: var(--el-border-radius-base);
box-shadow: var(--el-box-shadow-light);
}
.nav-menus {
display: flex;
align-items: center;
height: 100%;
margin-left: auto;
background-color: v-bind('configStore.getColorVal("headerBarBackground")');
.nav-menu-item {
height: 100%;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.nav-menu-icon {
box-sizing: content-box;
color: v-bind('configStore.getColorVal("headerBarTabColor")');
}
&:hover {
.icon {
animation: twinkle 0.3s ease-in-out;
}
}
}
.admin-info {
display: flex;
height: 100%;
padding: 0 10px;
align-items: center;
cursor: pointer;
user-select: none;
color: v-bind('configStore.getColorVal("headerBarTabColor")');
}
.admin-name {
padding-left: 6px;
white-space: nowrap;
}
.nav-menu-item:hover,
.admin-info:hover,
.nav-menu-item.hover,
.admin-info.hover {
background: v-bind('configStore.getColorVal("headerBarHoverBackground")');
}
}
.dropdown-menu-box :deep(.el-dropdown-menu__item) {
justify-content: center;
}
.admin-info-base {
display: flex;
justify-content: center;
flex-wrap: wrap;
padding-top: 10px;
.admin-info-other {
display: block;
width: 100%;
text-align: center;
padding: 10px 0;
.admin-info-name {
font-size: var(--el-font-size-large);
}
}
}
.admin-info-footer {
padding: 10px 0;
margin: 0 -12px -12px -12px;
display: flex;
justify-content: space-around;
}
.pt2 {
padding-top: 2px;
}
@keyframes twinkle {
0% {
transform: scale(0);
}
80% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<el-container class="layout-container">
<Aside />
<el-container class="content-wrapper">
<Header />
<Main />
</el-container>
</el-container>
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
</template>
<script setup lang="ts">
import Aside from '@/layouts/admin/components/aside.vue'
import Header from '@/layouts/admin/components/header.vue'
import Main from '@/layouts/admin/router-view/main.vue'
import CloseFullScreen from '@/layouts/admin/components/closeFullScreen.vue'
import { useNavTabs } from '@/stores/navTabs'
const navTabs = useNavTabs()
</script>
<style scoped>
.layout-container {
height: 100%;
width: 100%;
}
.content-wrapper {
flex-direction: column;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<el-container class="layout-container">
<Aside />
<el-container class="content-wrapper">
<Header />
<Main />
</el-container>
</el-container>
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
</template>
<script setup lang="ts">
import Aside from '@/layouts/admin/components/aside.vue'
import Header from '@/layouts/admin/components/header.vue'
import Main from '@/layouts/admin/router-view/main.vue'
import CloseFullScreen from '@/layouts/admin/components/closeFullScreen.vue'
import { useNavTabs } from '@/stores/navTabs'
const navTabs = useNavTabs()
</script>
<style scoped>
.layout-container {
height: 100%;
width: 100%;
}
.content-wrapper {
flex-direction: column;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<el-container class="layout-container">
<Aside />
<el-container class="content-wrapper">
<Header />
<Main />
</el-container>
</el-container>
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
</template>
<script setup lang="ts">
import Aside from '@/layouts/admin/components/aside.vue'
import Header from '@/layouts/admin/components/header.vue'
import Main from '@/layouts/admin/router-view/main.vue'
import CloseFullScreen from '@/layouts/admin/components/closeFullScreen.vue'
import { useNavTabs } from '@/stores/navTabs'
const navTabs = useNavTabs()
</script>
<style scoped>
.layout-container {
height: 100%;
width: 100%;
}
.content-wrapper {
flex-direction: column;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,29 @@
<template>
<el-container class="layout-container">
<el-container class="content-wrapper">
<Header />
<Main />
</el-container>
</el-container>
<CloseFullScreen v-if="navTabs.state.tabFullScreen" />
</template>
<script setup lang="ts">
import Header from '@/layouts/admin/components/header.vue'
import Main from '@/layouts/admin/router-view/main.vue'
import CloseFullScreen from '@/layouts/admin/components/closeFullScreen.vue'
import { useNavTabs } from '@/stores/navTabs'
const navTabs = useNavTabs()
</script>
<style scoped>
.layout-container {
height: 100%;
width: 100%;
}
.content-wrapper {
flex-direction: column;
width: 100%;
height: 100%;
}
</style>

124
src/layouts/admin/index.vue Normal file
View File

@@ -0,0 +1,124 @@
<template>
<component :is="config.layout.layoutMode"></component>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
import { useAdminInfo } from '@/stores/adminInfo'
import { useRoute } from 'vue-router'
import Default from '@/layouts/admin/container/default.vue'
import Classic from '@/layouts/admin/container/classic.vue'
import Streamline from '@/layouts/admin/container/streamline.vue'
import Double from '@/layouts/admin/container/double.vue'
import { onMounted, onBeforeMount } from 'vue'
import { handleAdminRoute, getFirstRoute, routePush } from '@/utils/router'
import router from '@/router/index'
import { useEventListener } from '@vueuse/core'
import { isEmpty } from 'lodash-es'
import { setNavTabsWidth } from '@/utils/layout'
import { adminBaseRoutePath } from '@/router/static'
defineOptions({
components: { Default, Classic, Streamline, Double }
})
const navTabs = useNavTabs()
const config = useConfig()
const route = useRoute()
const adminInfo = useAdminInfo()
const state = reactive({
autoMenuCollapseLock: false
})
onMounted(() => {
// if (!adminInfo.token) return router.push({ name: 'login' })
init()
setNavTabsWidth()
useEventListener(window, 'resize', setNavTabsWidth)
})
onBeforeMount(() => {
onAdaptiveLayout()
useEventListener(window, 'resize', onAdaptiveLayout)
})
const init = () => {
/**
* 后台初始化请求,获取站点配置,动态路由等信息
*/
handleAdminRoute([
{
id: 1,
pid: 0,
type: 'menu',
title: '控制台',
name: 'dashboard',
path: 'dashboard',
icon: 'fa fa-dashboard',
menu_type: 'tab',
url: '',
component: '/src/views/dashboard/index.vue',
keepalive: 'dashboard',
extend: 'none',
children: [
{
id: 94,
pid: 1,
type: 'button',
title: '查看',
name: 'dashboard/index',
path: '',
icon: '',
menu_type: null,
url: '',
component: '',
keepalive: 0,
extend: 'none'
}
]
}
])
// 预跳转到上次路径
if (route.params.to) {
const lastRoute = JSON.parse(route.params.to as string)
if (lastRoute.path != adminBaseRoutePath) {
let query = !isEmpty(lastRoute.query) ? lastRoute.query : {}
routePush({ path: lastRoute.path, query: query })
return
}
}
// 跳转到第一个菜单
let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
if (firstRoute) routePush(firstRoute.path)
}
const onAdaptiveLayout = () => {
let defaultBeforeResizeLayout = {
layoutMode: config.layout.layoutMode,
menuCollapse: config.layout.menuCollapse
}
const clientWidth = document.body.clientWidth
if (clientWidth < 1024) {
/**
* 锁定窗口改变自动调整 menuCollapse
* 避免已是小窗且打开了菜单栏时,意外的自动关闭菜单栏
*/
if (!state.autoMenuCollapseLock) {
state.autoMenuCollapseLock = true
config.setLayout('menuCollapse', true)
}
config.setLayout('shrink', true)
config.setLayoutMode('Classic')
} else {
state.autoMenuCollapseLock = false
config.setLayout('menuCollapse', defaultBeforeResizeLayout.menuCollapse)
config.setLayout('shrink', false)
config.setLayoutMode(defaultBeforeResizeLayout.layoutMode)
}
}
</script>

View File

@@ -0,0 +1,104 @@
<template>
<el-main class="layout-main">
<el-scrollbar class="layout-main-scrollbar" :style="layoutMainScrollbarStyle()" ref="mainScrollbarRef">
<router-view v-slot="{ Component }">
<transition :name="config.layout.mainAnimation" mode="out-in">
<keep-alive :include="state.keepAliveComponentNameList">
<component :is="Component" :key="state.componentKey" />
</keep-alive>
</transition>
</router-view>
</el-scrollbar>
</el-main>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch, onBeforeMount, onUnmounted, nextTick, provide } from 'vue'
import { useRoute, type RouteLocationNormalized } from 'vue-router'
import { mainHeight as layoutMainScrollbarStyle } from '@/utils/layout'
import useCurrentInstance from '@/utils/useCurrentInstance'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
import type { ScrollbarInstance } from 'element-plus'
defineOptions({
name: 'layout/main',
})
const { proxy } = useCurrentInstance()
const route = useRoute()
const config = useConfig()
const navTabs = useNavTabs()
const mainScrollbarRef = ref<ScrollbarInstance>()
const state: {
componentKey: string
keepAliveComponentNameList: string[]
} = reactive({
componentKey: route.path,
keepAliveComponentNameList: [],
})
const addKeepAliveComponentName = function (keepAliveName: string | undefined) {
if (keepAliveName) {
let exist = state.keepAliveComponentNameList.find((name: string) => {
return name === keepAliveName
})
if (exist) return
state.keepAliveComponentNameList.push(keepAliveName)
}
}
onBeforeMount(() => {
proxy.eventBus.on('onTabViewRefresh', (menu: RouteLocationNormalized) => {
state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter((name: string) => menu.meta.keepalive !== name)
state.componentKey = ''
nextTick(() => {
state.componentKey = menu.path
addKeepAliveComponentName(menu.meta.keepalive as string)
})
})
proxy.eventBus.on('onTabViewClose', (menu: RouteLocationNormalized) => {
state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter((name: string) => menu.meta.keepalive !== name)
})
})
onUnmounted(() => {
proxy.eventBus.off('onTabViewRefresh')
proxy.eventBus.off('onTabViewClose')
})
onMounted(() => {
// 确保刷新页面时也能正确取得当前路由 keepalive 参数
if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
}
})
watch(
() => route.path,
() => {
state.componentKey = route.path
if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
}
}
)
provide('mainScrollbarRef', mainScrollbarRef)
</script>
<style scoped lang="scss">
.layout-container .layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
height: 100%;
}
.layout-main-scrollbar {
width: 100%;
position: relative;
overflow: hidden;
}
</style>