first
This commit is contained in:
53
src/layouts/admin/components/aside.vue
Normal file
53
src/layouts/admin/components/aside.vue
Normal 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>
|
||||
71
src/layouts/admin/components/closeFullScreen.vue
Normal file
71
src/layouts/admin/components/closeFullScreen.vue
Normal 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>
|
||||
391
src/layouts/admin/components/config.vue
Normal file
391
src/layouts/admin/components/config.vue
Normal 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>
|
||||
28
src/layouts/admin/components/header.vue
Normal file
28
src/layouts/admin/components/header.vue
Normal 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>
|
||||
77
src/layouts/admin/components/logo.vue
Normal file
77
src/layouts/admin/components/logo.vue
Normal file
File diff suppressed because one or more lines are too long
18
src/layouts/admin/components/menus/helper.ts
Normal file
18
src/layouts/admin/components/menus/helper.ts
Normal 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
|
||||
}
|
||||
111
src/layouts/admin/components/menus/menuHorizontal.vue
Normal file
111
src/layouts/admin/components/menus/menuHorizontal.vue
Normal 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>
|
||||
81
src/layouts/admin/components/menus/menuTree.vue
Normal file
81
src/layouts/admin/components/menus/menuTree.vue
Normal 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>
|
||||
81
src/layouts/admin/components/menus/menuVertical.vue
Normal file
81
src/layouts/admin/components/menus/menuVertical.vue
Normal 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>
|
||||
101
src/layouts/admin/components/menus/menuVerticalChildren.vue
Normal file
101
src/layouts/admin/components/menus/menuVerticalChildren.vue
Normal 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>
|
||||
75
src/layouts/admin/components/nav.vue
Normal file
75
src/layouts/admin/components/nav.vue
Normal 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>
|
||||
99
src/layouts/admin/components/navBar/classic.vue
Normal file
99
src/layouts/admin/components/navBar/classic.vue
Normal 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>
|
||||
65
src/layouts/admin/components/navBar/default.vue
Normal file
65
src/layouts/admin/components/navBar/default.vue
Normal 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>
|
||||
102
src/layouts/admin/components/navBar/double.vue
Normal file
102
src/layouts/admin/components/navBar/double.vue
Normal 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>
|
||||
239
src/layouts/admin/components/navBar/tabs.vue
Normal file
239
src/layouts/admin/components/navBar/tabs.vue
Normal 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>
|
||||
232
src/layouts/admin/components/navMenus.vue
Normal file
232
src/layouts/admin/components/navMenus.vue
Normal 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>
|
||||
54
src/layouts/admin/components/popup/adminInfo.vue
Normal file
54
src/layouts/admin/components/popup/adminInfo.vue
Normal 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>
|
||||
109
src/layouts/admin/components/popup/password.vue
Normal file
109
src/layouts/admin/components/popup/password.vue
Normal 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>
|
||||
33
src/layouts/admin/container/classic.vue
Normal file
33
src/layouts/admin/container/classic.vue
Normal 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>
|
||||
31
src/layouts/admin/container/default.vue
Normal file
31
src/layouts/admin/container/default.vue
Normal 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>
|
||||
31
src/layouts/admin/container/double.vue
Normal file
31
src/layouts/admin/container/double.vue
Normal 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>
|
||||
29
src/layouts/admin/container/streamline.vue
Normal file
29
src/layouts/admin/container/streamline.vue
Normal 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
118
src/layouts/admin/index.vue
Normal 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>
|
||||
104
src/layouts/admin/router-view/main.vue
Normal file
104
src/layouts/admin/router-view/main.vue
Normal 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>
|
||||
75
src/layouts/common/components/darkSwitch.vue
Normal file
75
src/layouts/common/components/darkSwitch.vue
Normal 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>
|
||||
58
src/layouts/common/components/loading.vue
Normal file
58
src/layouts/common/components/loading.vue
Normal 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>
|
||||
37
src/layouts/common/router-view/iframe.vue
Normal file
37
src/layouts/common/router-view/iframe.vue
Normal 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>
|
||||
Reference in New Issue
Block a user