This commit is contained in:
仲么了
2024-02-19 13:44:32 +08:00
commit 361cbb713d
238 changed files with 202544 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: 10px 0 10px 10px;
height: calc(100vh - 20px);
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: 50px;
width: 100px;
left: calc(50% - 50px);
}
</style>

View File

@@ -0,0 +1,391 @@
<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-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>
<el-form-item label="组件主题色">
<el-color-picker
@change="onCommitColorState($event, 'elementUiPrimary')"
:model-value="configStore.getColorVal('elementUiPrimary')"
/>
</el-form-item>
<el-form-item label="表格标题栏背景颜色">
<el-color-picker
@change="onCommitColorState($event, 'tableHeaderBackground')"
:model-value="configStore.getColorVal('tableHeaderBackground')"
/>
</el-form-item>
<el-form-item label="表格标题栏文字颜色">
<el-color-picker
@change="onCommitColorState($event, 'tableHeaderColor')"
:model-value="configStore.getColorVal('tableHeaderColor')"
/>
</el-form-item>
<el-form-item label="表格激活栏颜色">
<el-color-picker
@change="onCommitColorState($event, 'tableCurrent')"
:model-value="configStore.getColorVal('tableCurrent')"
/>
</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="确定要恢复全部配置到默认值吗?">
<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>

File diff suppressed because one or more lines are too long

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,111 @@
<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: 50px;
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-menu-hover-color: v-bind('config.getColorVal("menuActiveBackground")');
}
.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,81 @@
<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")');
--el-menu-hover-color: v-bind('config.getColorVal("menuActiveBackground")');
}
</style>

View File

@@ -0,0 +1,101 @@
<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")');
--el-menu-hover-color: v-bind('config.getColorVal("menuActiveBackground")');
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class='nav-bar'>
<NavTabs />
</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: 45px;
width: 100%;
padding: 10px 10px 0;
: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;
background: #fff;
color:#111;
margin-right: 10px;
transition: all .2s;
.close-icon {
padding: 2px;
margin: 2px 0 0 4px;
}
&.active {
color: #fff;
background: var(--el-color-primary);
.close-icon {
color: #fff !important;
}
}
}
.nav-tabs-active-box {
position: absolute;
height: 50px;
//background: var(--el-color-primary);
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,99 @@
<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>
<span class='nav-bar-title'>电能质量数据监测云平台</span>
<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;
align-items: center;
height: 60px;
width: 100%;
background-color: v-bind('config.getColorVal("headerBarBackground")');
.nav-bar-title {
color: v-bind('config.getColorVal("headerBarTabColor")');
font-size: 24px;
margin-left: 10px;
font-weight: 700
}
: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;
color: v-bind('config.getColorVal("headerBarTabColor")') !important;
}
&.active {
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
.close-icon {
color: v-bind('config.getColorVal("headerBarTabActiveColor")') !important;;
}
}
&:hover {
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
background-color: v-bind('config.getColorVal("headerBarHoverBackground")');
.close-icon {
color: v-bind('config.getColorVal("headerBarTabActiveColor")') !important;;
}
}
}
.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,65 @@
<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: 40px;
margin: 10px 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;
color: v-bind('config.getColorVal("headerBarTabColor")') !important;
}
&.active {
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
.close-icon {
color: v-bind('config.getColorVal("headerBarTabActiveColor")') !important;;
}
}
&:hover {
color: v-bind('config.getColorVal("headerBarTabActiveColor")');
background-color: v-bind('config.getColorVal("headerBarHoverBackground")');
.close-icon {
color: v-bind('config.getColorVal("headerBarTabActiveColor")') !important;;
}
}
}
.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,102 @@
<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: 50px;
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-menu-hover-color: v-bind('config.getColorVal("menuActiveBackground")');
}
.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,239 @@
<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,232 @@
<template>
<div class="nav-menus" :class="configStore.layout.layoutMode">
<div @click="savePng" class="nav-menu-item">
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
name="el-icon-Camera"
size="18"
/>
</div>
<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="fa-solid fa-compress"
size="18"
/>
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
v-else
name="fa-solid fa-expand"
size="18"
/>
</div>
<el-dropdown style='height: 100%;' @command='handleCommand'>
<div class="admin-info" :class="state.currentNavMenu == 'adminInfo' ? 'hover' : ''">
<el-avatar :size="25" fit="fill">
<img src="@/assets/avatar.png" alt="" />
</el-avatar>
<div class="admin-name">{{ adminInfo.nickname }}</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="adminInfo">个人资料</el-dropdown-item>
<el-dropdown-item command="changePwd">修改密码</el-dropdown-item>
<el-dropdown-item command="layout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<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 />
<PopupPwd ref='popupPwd' />
<AdminInfo ref='popupAdminInfo' />
<!-- <TerminalVue /> -->
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } 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 router from '@/router'
import { routePush } from '@/utils/router'
import { fullUrl } from '@/utils/common'
import html2canvas from 'html2canvas'
import PopupPwd from './popup/password.vue'
import AdminInfo from './popup/adminInfo.vue'
const adminInfo = useAdminInfo()
const configStore = useConfig()
const popupPwd = ref()
const popupAdminInfo = ref()
const state = reactive({
isFullScreen: false,
currentNavMenu: '',
showLayoutDrawer: false,
showAdminInfoPopover: false
})
const savePng = () => {
html2canvas(document.body, {
scale: 1,
useCORS: true
}).then(function (canvas) {
var link = document.createElement('a')
link.href = canvas.toDataURL('image/png')
link.download = 'screenshot.png'
link.click()
})
}
const onFullScreen = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('layouts.Full screen is not supported')
return false
}
screenfull.toggle()
screenfull.onchange(() => {
state.isFullScreen = screenfull.isFullscreen
})
}
const handleCommand = (key: string) => {
console.log(key)
switch (key) {
case 'adminInfo':
popupAdminInfo.value.open()
break
case 'changePwd':
popupPwd.value.open()
break
case 'layout':
router.push({ name: 'login' })
break
default:
break
}
}
</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")');
&:hover {
color: v-bind('configStore.getColorVal("headerBarTabActiveColor")');
}
}
.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")');
.nav-menu-icon {
color: v-bind('configStore.getColorVal("headerBarTabActiveColor")') !important;
}
}
}
.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,54 @@
<template>
<el-dialog class='cn-operate-dialog' v-model='dialogVisible' :title='title'>
<el-scrollbar>
<el-form :inline='false' :model='form' label-width='120px'>
<el-form-item label='用户名称:'>
<el-input v-model='form.name' :disabled='true'></el-input>
</el-form-item>
<el-form-item label='登录名称:' class='top'>
<el-input v-model='form.loginName' :disabled='true'></el-input>
</el-form-item>
<el-form-item label='归属部门名称:' class='top'>
<el-input v-model='form.deptName' :disabled='true'></el-input>
</el-form-item>
<el-form-item label='拥有的角色:' class='top'>
<el-input v-model='form.role' :disabled='true'></el-input>
</el-form-item>
<el-form-item label='电话号码:' class='top'>
<el-input v-model='form.phone' :disabled='true'></el-input>
</el-form-item>
<el-form-item label='电子邮箱:' class='top'>
<el-input v-model='form.email' :disabled='true'></el-input>
</el-form-item>
</el-form>
</el-scrollbar>
</el-dialog>
</template>
<script lang='ts' setup>
import { ref, inject } from 'vue'
import { reactive } from 'vue'
import { useAdminInfo } from '@/stores/adminInfo'
const dialogVisible = ref(false)
const title = ref('用户信息')
const adminInfo = useAdminInfo()
const formRef = ref()
const form = reactive({
name: '',
deptName: '',
phone: '',
email: '',
role: '',
loginName: ''
})
const open = () => {
dialogVisible.value = true
for (const key in form) {
form[key] = Array.isArray(adminInfo.$state[key]) ? adminInfo.$state[key].join(',') : adminInfo.$state[key]
}
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,109 @@
<template>
<el-dialog class="cn-operate-dialog" v-model="dialogVisible" :title="title">
<el-scrollbar>
<el-form :inline="false" :model="form" label-width="120px" :rules="rules" ref="formRef">
<el-form-item label="校验密码:" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入校验密码" show-password />
</el-form-item>
<el-form-item label="新密码:" prop="newPwd" style="margin-top: 20px">
<el-input v-model="form.newPwd" type="password" placeholder="请输入新密码" show-password />
</el-form-item>
<el-form-item label="确认密码:" prop="confirmPwd" style="margin-top: 20px">
<el-input v-model="form.confirmPwd" type="password" placeholder="请输入确认密码" show-password />
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, inject } from 'vue'
import { reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { passwordConfirm, updatePassword } from '@/api/user-boot/user'
import { validatePwd } from '@/utils/common'
import { useAdminInfo } from '@/stores/adminInfo'
const adminInfo = useAdminInfo()
const dialogVisible = ref(false)
const title = ref('修改密码')
const formRef = ref()
// 注意不要和表单ref的命名冲突
const form = reactive({
password: '',
newPwd: '',
confirmPwd: ''
})
const rules = {
password: [
{ required: true, message: '请输入校验密码', trigger: 'blur' },
{
min: 6,
max: 16,
message: '长度在 6 到 16 个字符',
trigger: 'blur'
}
],
newPwd: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
min: 6,
max: 16,
message: '长度在 6 到 16 个字符',
trigger: 'blur'
},
{ validator: validatePwd, trigger: 'blur' }
],
confirmPwd: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{
min: 6,
max: 16,
message: '长度在 6 到 16 个字符',
trigger: 'blur'
},
{
validator: (rule: any, value: string, callback: any) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== form.newPwd) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
},
trigger: 'blur',
required: true
}
]
}
const open = () => {
dialogVisible.value = true
form.password = ''
form.newPwd = ''
form.confirmPwd = ''
}
const submit = () => {
formRef.value.validate(async (valid: boolean) => {
if (valid) {
passwordConfirm(form.password).then(res => {
updatePassword({
id: adminInfo.$state.userIndex,
newPassword: form.newPwd
}).then((res: any) => {
ElMessage.success('密码修改成功')
dialogVisible.value = false
})
})
}
})
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,33 @@
<template>
<el-container class="layout-container">
<Aside />
<el-container class="content-wrapper">
<Header />
<Nav />
<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'
import Nav from '@/layouts/admin/components/nav.vue'
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>

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

@@ -0,0 +1,118 @@
<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 { useDictData } from '@/stores/dictData'
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'
import { getRouteMenu, dictDataCache } from '@/api/auth'
import { getAreaList } from '@/api/common'
import { BasicDictData } from '@/stores/interface'
import { getUserById } from '@/api/user-boot/user'
defineOptions({
components: { Default, Classic, Streamline, Double }
})
const navTabs = useNavTabs()
const config = useConfig()
const route = useRoute()
const adminInfo = useAdminInfo()
const dictData = useDictData()
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 = async () => {
await Promise.all([getAreaList(), dictDataCache(),getUserById()]).then(res => {
dictData.state.area = res[0].data
dictData.state.basic = res[1].data
adminInfo.dataFill(res[2].data)
})
/**
* 后台初始化请求,获取站点配置,动态路由等信息
*/
getRouteMenu().then((res: any) => {
const handlerMenu = (data: any) => {
data.forEach((item: any) => {
item.routePath = item.routePath[0] == '/' ? item.routePath.substring(1, item.routePath.length) : item.routePath
item.path = item.routePath
item.name = item.routePath
item.keepalive = item.routePath
item.component = item.routeName || '/src/views/Event-boot/Region/overview.vue'
item.type = item.children && item.children.length > 0 ? 'menu_dir' : 'menu'
item.menu_type = item.children && item.children.length > 0 ? null : 'tab'
if (item.children) {
handlerMenu(item.children)
}
})
}
handlerMenu(res.data)
handleAdminRoute(res.data)
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" :style="mainHeight()">
<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-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 } from '@/utils/layout'
import useCurrentInstance from '@/utils/useCurrentInstance'
import { useConfig } from '@/stores/config'
import { useNavTabs } from '@/stores/navTabs'
defineOptions({
name: 'layout/main'
})
const { proxy } = useCurrentInstance()
const route = useRoute()
const config = useConfig()
const navTabs = useNavTabs()
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)
}
}
)
</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>

View File

@@ -0,0 +1,75 @@
<template>
<div class="theme-toggle-content">
<div class="switch">
<div class="switch-action">
<Icon name="local-dark" color="#f2f2f2" size="13px" class="switch-icon dark-icon" />
<Icon name="local-light" color="#303133" size="13px" class="switch-icon light-icon" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.theme-toggle-content {
display: flex;
align-items: center;
height: 24px;
padding: 0 12px;
}
.switch {
display: inline-block;
position: relative;
width: 40px;
height: 20px;
border: 1px solid var(--el-border-color);
border-radius: 10px;
box-sizing: border-box;
background-color: var(--ba-bg-color);
cursor: pointer;
transition: border-color 0.3s, background-color 0.5s;
}
.switch-action {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 1px;
left: 1px;
border-radius: 50%;
background-color: #ffffff;
transform: translate(0);
color: var(--el-text-color-primary);
transition: all 0.3s;
}
.switch-icon {
position: absolute;
left: 1px;
bottom: 1px;
transition: all 0.3s;
cursor: pointer;
}
.light-icon {
opacity: 1;
}
.dark-icon {
opacity: 0;
}
@at-root .dark {
.switch {
background-color: #2c2c2c;
}
.switch-action {
transform: translate(20px);
background-color: #141414;
}
.dark-icon {
opacity: 1;
}
.light-icon {
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div>
<div
v-loading="true"
element-loading-background="var(--ba-bg-color-overlay)"
element-loading-text="加载中"
class="default-main ba-main-loading"
></div>
<div v-if="state.showReload" class="loading-footer">
<el-button @click="refresh" type="warning">重新加载</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { onUnmounted, reactive } from 'vue'
import router from '@/router/index'
import { useNavTabs } from '@/stores/navTabs'
import { isAdminApp } from '@/utils/common'
import { getFirstRoute, routePush } from '@/utils/router'
let timer: number
const navTabs = useNavTabs()
const state = reactive({
maximumWait: 1000 * 6,
showReload: false,
})
const refresh = () => {
router.go(0)
}
// if (isAdminApp() && navTabs.state.tabsViewRoutes.length > 0) {
// let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
// if (firstRoute) routePush(firstRoute.path)
// }
timer = window.setTimeout(() => {
state.showReload = true
}, state.maximumWait)
onUnmounted(() => {
clearTimeout(timer)
})
</script>
<style scoped lang="scss">
.ba-main-loading {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.loading-footer {
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="iframe-main" v-loading="state.loading">
<iframe
:src="state.iframeSrc"
:style="iframeStyle(35)"
frameborder="0"
height="100%"
width="100%"
id="iframe"
ref="iframeRef"
@load="hideLoading"
></iframe>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { mainHeight as iframeStyle } from '@/utils/layout'
import { useRouter } from 'vue-router'
const router = useRouter()
const state = reactive({
loading: true,
iframeSrc: router.currentRoute.value.meta.url as string,
})
const hideLoading = () => {
state.loading = false
}
</script>
<style scoped lang="scss">
.iframe-main {
margin: var(--ba-main-space);
}
</style>