更新
This commit is contained in:
@@ -66,7 +66,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
min-height: 100vh;
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 暗色主题样式 ====================
|
// ==================== 暗色主题样式 ====================
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-breadcrumb separator="/">
|
<el-breadcrumb separator="/">
|
||||||
<transition-group name="breadcrumb">
|
<transition-group name="breadcrumb">
|
||||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
|
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
|
||||||
<span v-if="index === breadcrumbs.length - 1" class="no-redirect">
|
<span v-if="item.redirect === 'noRedirect' || index === levelList.length - 1" class="no-redirect">
|
||||||
{{ item.meta?.title }}
|
{{ item.meta.title }}
|
||||||
</span>
|
</span>
|
||||||
<a v-else class="redirect" @click.prevent="handleLink(item)">
|
<a v-else class="redirect" @click.prevent="handleLink(item)">
|
||||||
{{ item.meta?.title }}
|
{{ item.meta.title }}
|
||||||
</a>
|
</a>
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
@@ -14,9 +14,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
// import { useI18n } from '@/hooks/useI18n'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LayoutBreadcrumb',
|
name: 'LayoutBreadcrumb',
|
||||||
@@ -24,30 +23,36 @@ defineOptions({
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// const { t } = useI18n()
|
|
||||||
|
|
||||||
// 面包屑列表
|
const levelList = ref([])
|
||||||
const breadcrumbs = computed(() => {
|
|
||||||
const matched = route.matched.filter((item) => item.meta && item.meta.title)
|
const getBreadcrumb = () => {
|
||||||
|
let matched = route.matched.filter((item) => {
|
||||||
|
if (item.meta && item.meta.title) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
const first = matched[0]
|
const first = matched[0]
|
||||||
|
|
||||||
if (!first || !first.meta?.title) {
|
if (!isDashboard(first)) {
|
||||||
return []
|
matched = [{ path: '/dashboard', meta: { title: '首页' } }].concat(matched)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果第一个不是首页,添加首页
|
levelList.value = matched.filter(
|
||||||
// if (first.path !== '/') {
|
(item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||||
// matched.unshift({
|
)
|
||||||
// path: '/',
|
}
|
||||||
// meta: { title: '首页' },
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
return matched
|
const isDashboard = (route) => {
|
||||||
})
|
const name = route && route.name
|
||||||
|
if (!name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return name.toString().trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
// 处理点击
|
|
||||||
const handleLink = (item) => {
|
const handleLink = (item) => {
|
||||||
const { redirect, path } = item
|
const { redirect, path } = item
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
@@ -56,52 +61,46 @@ const handleLink = (item) => {
|
|||||||
}
|
}
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
getBreadcrumb()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.el-breadcrumb {
|
.app-breadcrumb.el-breadcrumb {
|
||||||
display: inline-flex;
|
display: inline-block;
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1;
|
line-height: 50px;
|
||||||
|
margin-left: 8px;
|
||||||
:deep(.el-breadcrumb__item) {
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
.el-breadcrumb__inner {
|
|
||||||
font-weight: 400;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child .el-breadcrumb__inner {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-redirect {
|
.no-redirect {
|
||||||
color: var(--el-text-color-primary);
|
color: #97a8be;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.redirect {
|
.redirect {
|
||||||
color: var(--el-text-color-regular);
|
color: #666;
|
||||||
cursor: pointer;
|
font-weight: 600;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--primary-color);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-enter-active,
|
.breadcrumb-enter-active,
|
||||||
.breadcrumb-leave-active {
|
.breadcrumb-leave-active {
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-enter-from,
|
.breadcrumb-enter-from,
|
||||||
.breadcrumb-leave-to {
|
.breadcrumb-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
|
|
||||||
<!-- 无子菜单 -->
|
<!-- 无子菜单 -->
|
||||||
<el-menu-item v-else-if="!menu.meta?.hidden" :index="menu.path">
|
<el-menu-item v-else-if="!menu.meta?.hidden" :index="menu.path" @click="handleMenuClick(menu)">
|
||||||
<el-icon v-if="menu.meta?.icon">
|
<el-icon v-if="menu.meta?.icon">
|
||||||
<component :is="menu.meta.icon" />
|
<component :is="menu.meta.icon" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
@@ -25,8 +25,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { defineProps } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MenuItem',
|
name: 'LayoutMenuItem',
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -40,60 +43,25 @@ defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 判断是否有子菜单
|
// 判断是否有子菜单
|
||||||
const hasChildren = (menu) => {
|
const hasChildren = (menu) => {
|
||||||
return menu.children && menu.children.length > 0
|
return menu.children && menu.children.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理菜单点击
|
||||||
|
const handleMenuClick = (menu) => {
|
||||||
|
if (menu.meta?.link) {
|
||||||
|
// 外部链接
|
||||||
|
window.open(menu.meta.link, '_blank')
|
||||||
|
} else if (menu.path) {
|
||||||
|
// 内部路由
|
||||||
|
router.push(menu.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
// 子菜单项样式
|
// 菜单项样式继承自 Element Plus
|
||||||
:deep(.el-sub-menu) {
|
|
||||||
.el-sub-menu__title {
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-fill-color-light) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 子菜单内容区
|
|
||||||
.el-menu {
|
|
||||||
background-color: var(--el-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单项样式
|
|
||||||
.el-menu-item {
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-fill-color-light) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background-color: var(--el-color-primary-light-9) !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图标样式优化
|
|
||||||
:deep(.el-icon) {
|
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 箭头图标动画
|
|
||||||
:deep(.el-sub-menu__icon-arrow) {
|
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
.el-sub-menu.is-opened > .el-sub-menu__title & {
|
|
||||||
transform: rotateZ(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单文本样式
|
|
||||||
:deep(span) {
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-menu :default-active="activeMenu" :mode="menuMode" :collapse="isCollapse" :unique-opened="uniqueOpened" :router="true" :collapse-transition="false" @select="handleSelect">
|
|
||||||
<template v-for="menu in menuList" :key="menu.path">
|
|
||||||
<!-- 有子菜单 -->
|
|
||||||
<el-sub-menu v-if="hasChildren(menu) && !menu.meta?.hidden" :index="menu.path">
|
|
||||||
<template #title>
|
|
||||||
<el-icon v-if="menu.meta?.icon">
|
|
||||||
<component :is="menu.meta.icon" />
|
|
||||||
</el-icon>
|
|
||||||
<span>{{ menu.meta?.title }}</span>
|
|
||||||
</template>
|
|
||||||
<!-- 递归渲染子菜单 -->
|
|
||||||
<menu-item :menu-list="menu.children" :parent-path="menu.path" />
|
|
||||||
</el-sub-menu>
|
|
||||||
|
|
||||||
<!-- 无子菜单 -->
|
|
||||||
<el-menu-item v-else-if="!menu.meta?.hidden" :index="menu.path">
|
|
||||||
<el-icon v-if="menu.meta?.icon">
|
|
||||||
<component :is="menu.meta.icon" />
|
|
||||||
</el-icon>
|
|
||||||
<template #title>
|
|
||||||
<span>{{ menu.meta?.title }}</span>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
</template>
|
|
||||||
</el-menu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
|
||||||
import MenuItem from './menu-item.vue'
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: 'LayoutMenu',
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: 'vertical',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const layoutStore = useLayoutStore()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// 菜单模式
|
|
||||||
const menuMode = computed(() => props.mode)
|
|
||||||
|
|
||||||
// 是否折叠
|
|
||||||
const isCollapse = computed(() => layoutStore.sidebarCollapsed)
|
|
||||||
|
|
||||||
// 是否只展开一个子菜单
|
|
||||||
const uniqueOpened = computed(() => true)
|
|
||||||
|
|
||||||
// 当前激活的菜单
|
|
||||||
const activeMenu = computed(() => {
|
|
||||||
const { meta, path } = route
|
|
||||||
if (meta?.activeMenu) {
|
|
||||||
return meta.activeMenu
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
})
|
|
||||||
|
|
||||||
// 菜单列表
|
|
||||||
const menuList = computed(() => {
|
|
||||||
const menus = userStore.getMenu() || []
|
|
||||||
|
|
||||||
// 如果是默认布局(双栏布局),只显示当前选中的父菜单的子菜单
|
|
||||||
if (layoutStore.layoutMode === 'default' && layoutStore.selectedParentMenu) {
|
|
||||||
const selectedMenu = menus.find((m) => m.path === layoutStore.selectedParentMenu.path)
|
|
||||||
return selectedMenu?.children || []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他布局显示所有菜单
|
|
||||||
return menus
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断是否有子菜单
|
|
||||||
const hasChildren = (menu) => {
|
|
||||||
return menu.children && menu.children.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单选择处理
|
|
||||||
const handleSelect = (index) => {
|
|
||||||
console.log('Menu selected:', index)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.el-menu {
|
|
||||||
border-right: none;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
// 菜单项样式
|
|
||||||
:deep(.el-menu-item) {
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-fill-color-light) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background-color: var(--el-color-primary-light-9) !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 子菜单样式
|
|
||||||
:deep(.el-sub-menu__title) {
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-fill-color-light) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-sub-menu__icon-arrow {
|
|
||||||
transition: transform 0.3s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
|
||||||
transform: rotateZ(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图标样式
|
|
||||||
:deep(.el-icon) {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
font-size: 18px;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 折叠状态
|
|
||||||
&.el-menu--collapse {
|
|
||||||
:deep(.el-menu-item),
|
|
||||||
:deep(.el-sub-menu__title) {
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-icon) {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,141 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting-container">
|
<div class="drawer-container">
|
||||||
<!-- 设置按钮 -->
|
<el-drawer v-model="drawerVisible" title="布局设置" size="280px">
|
||||||
<div class="setting-btn" @click="showDrawer = true">
|
<el-scrollbar>
|
||||||
<el-icon>
|
<el-form label-width="100px" label-position="left">
|
||||||
<Setting />
|
<el-form-item label="布局模式">
|
||||||
|
<el-select v-model="layoutMode" placeholder="请选择" @change="handleLayoutModeChange">
|
||||||
|
<el-option label="默认布局" value="default" />
|
||||||
|
<el-option label="菜单布局" value="menu" />
|
||||||
|
<el-option label="顶部布局" value="top" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="主题颜色">
|
||||||
|
<el-color-picker v-model="themeColor" @change="handleThemeChange" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider>显示设置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="显示标签栏">
|
||||||
|
<el-switch v-model="showTags" @change="handleShowTagsChange" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="显示面包屑">
|
||||||
|
<el-switch v-model="showBreadcrumb" @change="handleShowBreadcrumbChange" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider>其他</el-divider>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleReset">重置设置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<div class="setting-btn" @click="drawerVisible = true">
|
||||||
|
<el-icon :size="24">
|
||||||
|
<component :is="'ElIconSetting'" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设置抽屉 -->
|
|
||||||
<el-drawer v-model="showDrawer" title="系统设置" :size="300" :close-on-click-modal="false">
|
|
||||||
<div class="setting-content">
|
|
||||||
<!-- 布局模式 -->
|
|
||||||
<div class="setting-item">
|
|
||||||
<div class="setting-title">布局模式</div>
|
|
||||||
<div class="setting-value">
|
|
||||||
<el-radio-group v-model="layoutMode" @change="handleLayoutModeChange">
|
|
||||||
<el-radio label="default">
|
|
||||||
<div class="layout-option">
|
|
||||||
<div class="layout-preview layout-default">
|
|
||||||
<div class="layout-aside-left"></div>
|
|
||||||
<div class="layout-aside-right"></div>
|
|
||||||
<div class="layout-main"></div>
|
|
||||||
</div>
|
|
||||||
<span>双栏布局</span>
|
|
||||||
</div>
|
|
||||||
</el-radio>
|
|
||||||
<el-radio label="menu">
|
|
||||||
<div class="layout-option">
|
|
||||||
<div class="layout-preview layout-menu">
|
|
||||||
<div class="layout-aside"></div>
|
|
||||||
<div class="layout-main"></div>
|
|
||||||
</div>
|
|
||||||
<span>菜单布局</span>
|
|
||||||
</div>
|
|
||||||
</el-radio>
|
|
||||||
<el-radio label="top">
|
|
||||||
<div class="layout-option">
|
|
||||||
<div class="layout-preview layout-top">
|
|
||||||
<div class="layout-header"></div>
|
|
||||||
<div class="layout-main"></div>
|
|
||||||
</div>
|
|
||||||
<span>顶部布局</span>
|
|
||||||
</div>
|
|
||||||
</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主题色 -->
|
|
||||||
<div class="setting-item">
|
|
||||||
<div class="setting-title">主题色</div>
|
|
||||||
<div class="setting-value">
|
|
||||||
<div class="color-picker-wrapper">
|
|
||||||
<el-color-picker v-model="themeColor" show-alpha :predefine="predefineColors" @change="handleThemeColorChange" />
|
|
||||||
</div>
|
|
||||||
<div class="color-presets">
|
|
||||||
<div v-for="color in predefineColors" :key="color" class="color-preset" :class="{ active: themeColor === color }" :style="{ backgroundColor: color }" @click="handleColorPresetClick(color)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主题模式 -->
|
|
||||||
<div class="setting-item">
|
|
||||||
<div class="setting-title">主题模式</div>
|
|
||||||
<div class="setting-value">
|
|
||||||
<el-radio-group v-model="isDark" @change="handleThemeModeChange">
|
|
||||||
<el-radio :label="false">
|
|
||||||
<el-icon>
|
|
||||||
<Sunny />
|
|
||||||
</el-icon>
|
|
||||||
浅色
|
|
||||||
</el-radio>
|
|
||||||
<el-radio :label="true">
|
|
||||||
<el-icon>
|
|
||||||
<Moon />
|
|
||||||
</el-icon>
|
|
||||||
深色
|
|
||||||
</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 标签栏 -->
|
|
||||||
<div class="setting-item">
|
|
||||||
<div class="setting-title">
|
|
||||||
<span>显示标签栏</span>
|
|
||||||
<el-tooltip content="开启后页面顶部显示标签页" placement="top">
|
|
||||||
<el-icon>
|
|
||||||
<QuestionFilled />
|
|
||||||
</el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="setting-value">
|
|
||||||
<el-switch v-model="showTags" @change="handleShowTagsChange" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 面包屑 -->
|
|
||||||
<div class="setting-item">
|
|
||||||
<div class="setting-title">
|
|
||||||
<span>显示面包屑</span>
|
|
||||||
<el-tooltip content="开启后页面顶部显示面包屑导航" placement="top">
|
|
||||||
<el-icon>
|
|
||||||
<QuestionFilled />
|
|
||||||
</el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="setting-value">
|
|
||||||
<el-switch v-model="showBreadcrumb" @change="handleShowBreadcrumbChange" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
|
||||||
<div class="setting-actions">
|
|
||||||
<el-button type="primary" @click="handleSave">
|
|
||||||
<el-icon>
|
|
||||||
<Check />
|
|
||||||
</el-icon>
|
|
||||||
保存配置
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleReset">
|
|
||||||
<el-icon>
|
|
||||||
<RefreshLeft />
|
|
||||||
</el-icon>
|
|
||||||
重置默认
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { useLayoutStore } from '../../stores/modules/layout'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Setting, Sunny, Moon, QuestionFilled, Check, RefreshLeft } from '@element-plus/icons-vue'
|
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LayoutSetting',
|
name: 'LayoutSetting',
|
||||||
@@ -143,350 +53,94 @@ defineOptions({
|
|||||||
|
|
||||||
const layoutStore = useLayoutStore()
|
const layoutStore = useLayoutStore()
|
||||||
|
|
||||||
// 抽屉显示状态
|
const drawerVisible = ref(false)
|
||||||
const showDrawer = ref(false)
|
|
||||||
|
|
||||||
// 预定义颜色
|
|
||||||
const predefineColors = [
|
|
||||||
'#1890ff',
|
|
||||||
'#409eff',
|
|
||||||
'#67c23a',
|
|
||||||
'#e6a23c',
|
|
||||||
'#f56c6c',
|
|
||||||
'#909399',
|
|
||||||
'#722ed1',
|
|
||||||
'#eb2f96',
|
|
||||||
'#52c41a',
|
|
||||||
'#1890ff',
|
|
||||||
'#2f54eb',
|
|
||||||
'#722ed1',
|
|
||||||
'#f5222d',
|
|
||||||
'#fa541c',
|
|
||||||
'#fa8c16',
|
|
||||||
'#faad14',
|
|
||||||
'#fadb14',
|
|
||||||
'#a0d911',
|
|
||||||
'#52c41a',
|
|
||||||
'#13c2c2',
|
|
||||||
'#1890ff',
|
|
||||||
'#2f54eb',
|
|
||||||
'#722ed1',
|
|
||||||
]
|
|
||||||
|
|
||||||
// 布局模式
|
// 布局模式
|
||||||
const layoutMode = computed({
|
const layoutMode = computed({
|
||||||
get: () => layoutStore.layoutMode,
|
get: () => layoutStore.layoutMode,
|
||||||
set: (val) => layoutStore.setLayoutMode(val),
|
set: (value) => {
|
||||||
|
layoutStore.setLayoutMode(value)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 主题色
|
// 主题颜色
|
||||||
const themeColor = ref(layoutStore.themeColor)
|
const themeColor = computed({
|
||||||
|
get: () => layoutStore.themeColor,
|
||||||
// 主题模式(深色/浅色)
|
set: (value) => {
|
||||||
const isDark = ref(document.documentElement.classList.contains('dark'))
|
layoutStore.setThemeColor(value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// 显示标签栏
|
// 显示标签栏
|
||||||
const showTags = computed({
|
const showTags = computed({
|
||||||
get: () => layoutStore.showTags,
|
get: () => layoutStore.showTags,
|
||||||
set: (val) => layoutStore.setShowTags(val),
|
set: (value) => {
|
||||||
|
layoutStore.setShowTags(value)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 显示面包屑
|
// 显示面包屑
|
||||||
const showBreadcrumb = computed({
|
const showBreadcrumb = computed({
|
||||||
get: () => layoutStore.showBreadcrumb,
|
get: () => layoutStore.showBreadcrumb,
|
||||||
set: (val) => layoutStore.setShowBreadcrumb(val),
|
set: (value) => {
|
||||||
|
layoutStore.setShowBreadcrumb(value)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 布局模式变化
|
// 布局模式变化
|
||||||
const handleLayoutModeChange = (value) => {
|
const handleLayoutModeChange = (value) => {
|
||||||
console.log('Layout mode changed:', value)
|
layoutStore.setLayoutMode(value)
|
||||||
|
ElMessage.success('布局模式已切换')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主题色变化
|
// 主题颜色变化
|
||||||
const handleThemeColorChange = (value) => {
|
const handleThemeChange = (value) => {
|
||||||
if (value) {
|
layoutStore.setThemeColor(value)
|
||||||
layoutStore.setThemeColor(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预定义颜色点击
|
// 显示标签栏变化
|
||||||
const handleColorPresetClick = (color) => {
|
|
||||||
themeColor.value = color
|
|
||||||
layoutStore.setThemeColor(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主题模式变化
|
|
||||||
const handleThemeModeChange = (value) => {
|
|
||||||
if (value) {
|
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标签栏显示变化
|
|
||||||
const handleShowTagsChange = (value) => {
|
const handleShowTagsChange = (value) => {
|
||||||
layoutStore.setShowTags(value)
|
layoutStore.setShowTags(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 面包屑显示变化
|
// 显示面包屑变化
|
||||||
const handleShowBreadcrumbChange = (value) => {
|
const handleShowBreadcrumbChange = (value) => {
|
||||||
layoutStore.setShowBreadcrumb(value)
|
layoutStore.setShowBreadcrumb(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存配置
|
// 重置设置
|
||||||
const handleSave = () => {
|
|
||||||
ElMessage.success('配置已保存')
|
|
||||||
showDrawer.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置配置
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
layoutStore.resetTheme()
|
layoutStore.resetTheme()
|
||||||
themeColor.value = layoutStore.themeColor
|
ElMessage.success('设置已重置')
|
||||||
isDark.value = false
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
ElMessage.success('已重置为默认配置')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
themeColor.value = layoutStore.themeColor
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.setting-container {
|
.drawer-container {
|
||||||
// 设置按钮
|
|
||||||
.setting-btn {
|
.setting-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 0;
|
right: 0;
|
||||||
transform: translateY(-50%);
|
z-index: 9999;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--el-color-primary);
|
background: var(--primary-color);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 6px 0 0 6px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
cursor: pointer;
|
||||||
z-index: 9999;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s;
|
||||||
user-select: none;
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
.el-icon {
|
.el-icon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
transition: transform 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
width: 56px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(-50%) scale(0.95);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置内容
|
|
||||||
.setting-content {
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
.setting-item {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
.setting-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-value {
|
|
||||||
// 布局选项
|
|
||||||
:deep(.el-radio-group) {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.el-radio {
|
|
||||||
margin-right: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-radio__label {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 2px solid var(--el-border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
background-color: var(--el-fill-color-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 布局预览
|
|
||||||
.layout-preview {
|
|
||||||
width: 80px;
|
|
||||||
height: 60px;
|
|
||||||
background: var(--el-fill-color-blank);
|
|
||||||
border: 2px solid var(--el-border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.layout-default {
|
|
||||||
.layout-aside-left {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 20px;
|
|
||||||
background: var(--el-color-primary-light-7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-aside-right {
|
|
||||||
position: absolute;
|
|
||||||
left: 20px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 25px;
|
|
||||||
background: var(--el-color-primary-light-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-main {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 45px;
|
|
||||||
background: var(--el-fill-color-lighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.layout-menu {
|
|
||||||
.layout-aside {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 25px;
|
|
||||||
background: var(--el-color-primary-light-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-main {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 25px;
|
|
||||||
background: var(--el-fill-color-lighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.layout-top {
|
|
||||||
.layout-header {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 12px;
|
|
||||||
background: var(--el-color-primary-light-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-main {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 12px;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
background: var(--el-fill-color-lighter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 颜色选择器
|
|
||||||
.color-picker-wrapper {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
:deep(.el-color-picker__trigger) {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-presets {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.color-preset {
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 100%;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 操作按钮
|
|
||||||
.setting-actions {
|
|
||||||
margin-top: 32px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid var(--el-border-color-light);
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,214 +1,278 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tags-view-container">
|
<div class="tags-view-container">
|
||||||
<el-scrollbar class="tags-view-wrapper">
|
<div class="tags-view-wrapper" @wheel="handleWheel">
|
||||||
<router-link v-for="tag in visibleTags" :key="tag.fullPath" :class="isActive(tag) ? 'active' : ''" class="tags-view-item" :to="{ path: tag.fullPath }" @click.middle="closeTag(tag)" @contextmenu.prevent="openMenu(tag, $event)">
|
<div
|
||||||
{{ tag.title || tag.meta?.title }}
|
ref="scrollRef"
|
||||||
<el-icon v-if="!tag.meta?.affix" class="el-icon-close" @click.prevent.stop="closeTag(tag)">
|
class="scroll-content"
|
||||||
<Close />
|
:style="{ transform: `translateX(${scrollState.translateX}px)`, transition: scrollState.transition }"
|
||||||
</el-icon>
|
>
|
||||||
</router-link>
|
<router-link
|
||||||
</el-scrollbar>
|
v-for="(tag, index) in visitedViews"
|
||||||
|
:id="`scroll-li-${index}`"
|
||||||
|
:key="tag.path"
|
||||||
|
:class="isActive(tag) ? 'active' : ''"
|
||||||
|
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||||
|
class="tags-view-item"
|
||||||
|
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||||
|
@contextmenu.prevent="openMenu(tag, $event)"
|
||||||
|
>
|
||||||
|
{{ tag.title }}
|
||||||
|
<el-icon v-if="!isAffix(tag)" class="close-icon" @click.prevent.stop="closeSelectedTag(tag)">
|
||||||
|
<ElIconClose />
|
||||||
|
</el-icon>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||||
<li @click="refreshSelectedTag(selectedTag)">
|
<li @click="refreshSelectedTag(selectedTag)">
|
||||||
<el-icon>
|
<el-icon><ElIconRefresh /></el-icon>
|
||||||
<Refresh />
|
|
||||||
</el-icon>
|
|
||||||
刷新
|
刷新
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!selectedTag?.meta?.affix" @click="closeSelectedTag(selectedTag)">
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||||
<el-icon>
|
<el-icon><ElIconClose /></el-icon>
|
||||||
<Close />
|
|
||||||
</el-icon>
|
|
||||||
关闭
|
关闭
|
||||||
</li>
|
</li>
|
||||||
<li @click="closeOthersTags">
|
<li @click="closeOthersTags">
|
||||||
<el-icon>
|
<el-icon><ElIconCircleClose /></el-icon>
|
||||||
<CircleClose />
|
|
||||||
</el-icon>
|
|
||||||
关闭其他
|
关闭其他
|
||||||
</li>
|
</li>
|
||||||
<li @click="closeAllTags(selectedTag)">
|
<li @click="closeAllTags(selectedTag)">
|
||||||
<el-icon>
|
<el-icon><ElIconFolderDelete /></el-icon>
|
||||||
<CircleCloseFilled />
|
关闭所有
|
||||||
</el-icon>
|
|
||||||
关闭全部
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
import { useLayoutStore } from '../../stores/modules/layout'
|
||||||
import { Close, Refresh, CircleClose, CircleCloseFilled } from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LayoutTags',
|
name: 'TagsView',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const layoutStore = useLayoutStore()
|
const layoutStore = useLayoutStore()
|
||||||
|
|
||||||
// 右键菜单
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const top = ref(0)
|
const top = ref(0)
|
||||||
const left = ref(0)
|
const left = ref(0)
|
||||||
const selectedTag = ref(null)
|
const selectedTag = ref({})
|
||||||
const affixTags = ref([])
|
const affixTags = ref([])
|
||||||
|
const scrollRef = ref(null)
|
||||||
// 可见的标签(去重)
|
const scrollState = ref({
|
||||||
const visibleTags = computed(() => {
|
translateX: 0,
|
||||||
const tags = layoutStore.viewTags || []
|
transition: ''
|
||||||
// 使用 Map 根据 fullPath 去重,保留最后出现的
|
})
|
||||||
const tagMap = new Map()
|
|
||||||
tags.forEach((tag) => {
|
const visitedViews = computed(() => layoutStore.viewTags)
|
||||||
if (tag.fullPath) {
|
const activeTabIndex = computed(() => {
|
||||||
tagMap.set(tag.fullPath, tag)
|
const index = visitedViews.value.findIndex(view => view.path === route.path)
|
||||||
}
|
return index
|
||||||
})
|
|
||||||
return Array.from(tagMap.values())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否激活
|
|
||||||
const isActive = (tag) => {
|
const isActive = (tag) => {
|
||||||
return tag.fullPath === route.fullPath
|
return tag.path === route.path
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤固定的标签
|
const isAffix = (tag) => {
|
||||||
const filterAffixTags = (routes, basePath = '/') => {
|
return tag.meta && tag.meta.affix
|
||||||
let tags = []
|
}
|
||||||
routes.forEach((route) => {
|
|
||||||
if (route.meta?.affix) {
|
// 滚动逻辑
|
||||||
const tagPath = basePath + route.path
|
const setTransition = () => {
|
||||||
tags.push({
|
scrollState.value.transition = 'transform 0.5s cubic-bezier(0.15, 0, 0.15, 1)'
|
||||||
name: route.name,
|
setTimeout(() => {
|
||||||
fullPath: tagPath,
|
scrollState.value.transition = ''
|
||||||
path: tagPath,
|
}, 250)
|
||||||
title: route.meta?.title,
|
}
|
||||||
meta: route.meta,
|
|
||||||
})
|
const getCurrentTabElement = () => {
|
||||||
}
|
const index = activeTabIndex.value
|
||||||
if (route.children) {
|
return index >= 0 ? document.getElementById(`scroll-li-${index}`) : null
|
||||||
const tempTags = filterAffixTags(route.children, route.path + '/')
|
}
|
||||||
if (tempTags.length >= 1) {
|
|
||||||
tags = [...tags, ...tempTags]
|
const calculateScrollPosition = () => {
|
||||||
}
|
if (!scrollRef.value) return null
|
||||||
|
|
||||||
|
const scrollWrapper = scrollRef.value.parentElement
|
||||||
|
if (!scrollWrapper) return null
|
||||||
|
|
||||||
|
const scrollWidth = scrollWrapper.offsetWidth
|
||||||
|
const ulWidth = scrollRef.value.offsetWidth
|
||||||
|
const curTabEl = getCurrentTabElement()
|
||||||
|
|
||||||
|
if (!curTabEl) return null
|
||||||
|
|
||||||
|
const { offsetLeft, clientWidth } = curTabEl
|
||||||
|
const curTabRight = offsetLeft + clientWidth
|
||||||
|
const targetLeft = scrollWidth - curTabRight
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollWidth,
|
||||||
|
ulWidth,
|
||||||
|
offsetLeft,
|
||||||
|
clientWidth,
|
||||||
|
curTabRight,
|
||||||
|
targetLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoPositionTab = () => {
|
||||||
|
const positions = calculateScrollPosition()
|
||||||
|
if (!positions) return
|
||||||
|
|
||||||
|
const { scrollWidth, ulWidth, offsetLeft, curTabRight, targetLeft } = positions
|
||||||
|
const currentTranslateX = scrollState.value.translateX
|
||||||
|
|
||||||
|
if ((offsetLeft > Math.abs(currentTranslateX) && curTabRight <= scrollWidth) ||
|
||||||
|
(currentTranslateX < targetLeft && targetLeft < 0)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (curTabRight > scrollWidth) {
|
||||||
|
scrollState.value.translateX = Math.max(targetLeft - 6, scrollWidth - ulWidth)
|
||||||
|
} else if (offsetLeft < Math.abs(currentTranslateX)) {
|
||||||
|
scrollState.value.translateX = -offsetLeft
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return tags
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化标签
|
const adjustPositionAfterClose = () => {
|
||||||
const initTags = () => {
|
const positions = calculateScrollPosition()
|
||||||
affixTags.value = filterAffixTags(route.matched)
|
if (!positions) return
|
||||||
for (const tag of affixTags.value) {
|
|
||||||
if (tag.name) {
|
const { scrollWidth, ulWidth, offsetLeft, clientWidth } = positions
|
||||||
layoutStore.updateViewTags(tag)
|
const curTabLeft = offsetLeft + clientWidth
|
||||||
}
|
|
||||||
}
|
requestAnimationFrame(() => {
|
||||||
|
scrollState.value.translateX = curTabLeft > scrollWidth ? scrollWidth - ulWidth : 0
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加标签
|
|
||||||
const addTags = () => {
|
const addTags = () => {
|
||||||
if (route.name && !route.meta?.hidden) {
|
const { name } = route
|
||||||
|
if (name) {
|
||||||
layoutStore.updateViewTags({
|
layoutStore.updateViewTags({
|
||||||
name: route.name,
|
|
||||||
path: route.path,
|
path: route.path,
|
||||||
fullPath: route.fullPath,
|
fullPath: route.fullPath,
|
||||||
title: route.meta?.title,
|
name: name,
|
||||||
|
title: route.meta.title,
|
||||||
meta: route.meta,
|
meta: route.meta,
|
||||||
|
query: route.query,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭标签
|
const closeSelectedTag = (view) => {
|
||||||
const closeTag = (tag) => {
|
layoutStore.removeViewTags(view.fullPath)
|
||||||
layoutStore.removeViewTags(tag.fullPath)
|
setTransition()
|
||||||
if (isActive(tag)) {
|
nextTick(() => {
|
||||||
toLastView(tag)
|
adjustPositionAfterClose()
|
||||||
|
})
|
||||||
|
if (isActive(view)) {
|
||||||
|
toLastView(visitedViews.value, view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭选中的标签
|
const closeOthersTags = () => {
|
||||||
const closeSelectedTag = (tag) => {
|
router.push(selectedTag.value)
|
||||||
closeTag(tag)
|
layoutStore.viewTags = visitedViews.value.filter((tag) => {
|
||||||
|
return tag.meta && tag.meta.affix || tag.fullPath === selectedTag.value.fullPath
|
||||||
|
})
|
||||||
|
setTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到最后一个标签
|
const closeAllTags = (view) => {
|
||||||
const toLastView = (tag) => {
|
layoutStore.viewTags = visitedViews.value.filter((tag) => tag.meta && tag.meta.affix)
|
||||||
const views = visibleTags.value
|
setTransition()
|
||||||
const latestView = views.slice(-1)[0]
|
if (view && view.fullPath) {
|
||||||
|
toLastView(visitedViews.value, view)
|
||||||
|
} else {
|
||||||
|
toLastView(layoutStore.viewTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toLastView = (visitedViews, view) => {
|
||||||
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
if (latestView) {
|
if (latestView) {
|
||||||
router.push(latestView.fullPath)
|
router.push(latestView.fullPath)
|
||||||
} else {
|
} else {
|
||||||
// 如果没有标签,跳转到首页
|
if (view.name === 'Dashboard') {
|
||||||
router.push('/')
|
router.replace({ path: '/redirect' + view.fullPath })
|
||||||
|
} else {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭其他标签
|
const refreshSelectedTag = (view) => {
|
||||||
const closeOthersTags = () => {
|
layoutStore.removeViewTags(view.fullPath)
|
||||||
router.push(selectedTag.value.fullPath)
|
const { fullPath } = view
|
||||||
layoutStore.clearViewTags()
|
layoutStore.updateViewTags({
|
||||||
addTags()
|
path: view.path,
|
||||||
for (const tag of affixTags.value) {
|
fullPath: view.fullPath,
|
||||||
layoutStore.updateViewTags(tag)
|
name: view.name,
|
||||||
}
|
title: view.meta.title,
|
||||||
|
meta: view.meta,
|
||||||
|
query: view.query,
|
||||||
|
})
|
||||||
|
router.replace({
|
||||||
|
path: '/redirect' + fullPath
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭所有标签
|
|
||||||
const closeAllTags = () => {
|
|
||||||
layoutStore.clearViewTags()
|
|
||||||
for (const tag of affixTags.value) {
|
|
||||||
layoutStore.updateViewTags(tag)
|
|
||||||
}
|
|
||||||
if (affixTags.value.length > 0) {
|
|
||||||
router.push(affixTags.value[0].fullPath)
|
|
||||||
} else {
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新选中的标签
|
|
||||||
const refreshSelectedTag = (tag) => {
|
|
||||||
layoutStore.refreshTag()
|
|
||||||
router.replace({ path: tag.fullPath })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开右键菜单
|
|
||||||
const openMenu = (tag, e) => {
|
const openMenu = (tag, e) => {
|
||||||
|
const scrollWrapper = scrollRef.value?.parentElement
|
||||||
|
if (!scrollWrapper) return
|
||||||
|
|
||||||
const menuMinWidth = 105
|
const menuMinWidth = 105
|
||||||
const offsetLeft = e.clientX - container.getBoundingClientRect().left // container margin left
|
const offsetLeft = scrollWrapper.getBoundingClientRect().left || 0
|
||||||
const offsetWidth = container.offsetWidth - 100
|
const offsetWidth = scrollWrapper.offsetWidth || 0
|
||||||
const maxLeft = offsetWidth - menuMinWidth
|
const maxLeft = offsetWidth - menuMinWidth
|
||||||
left.value = offsetLeft > maxLeft ? maxLeft : offsetLeft
|
left.value = e.clientX - offsetLeft + 15
|
||||||
|
|
||||||
|
if (left.value > maxLeft) {
|
||||||
|
left.value = maxLeft
|
||||||
|
}
|
||||||
|
|
||||||
top.value = e.clientY
|
top.value = e.clientY
|
||||||
visible.value = true
|
visible.value = true
|
||||||
selectedTag.value = tag
|
selectedTag.value = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭右键菜单
|
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 容器引用
|
const handleWheel = (e) => {
|
||||||
let container = null
|
const scrollWrapper = scrollRef.value?.parentElement
|
||||||
|
if (!scrollWrapper) return
|
||||||
|
|
||||||
// 监听路由变化
|
const scrollWidth = scrollWrapper.offsetWidth
|
||||||
watch(
|
const tagElements = scrollRef.value?.querySelectorAll('.tags-view-item')
|
||||||
() => route.path,
|
if (!tagElements || tagElements.length === 0) return
|
||||||
() => {
|
|
||||||
addTags()
|
const ulWidth = scrollRef.value.offsetWidth
|
||||||
},
|
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||||
{ immediate: true },
|
const currentScroll = -scrollState.value.translateX
|
||||||
)
|
const newScroll = Math.max(0, Math.min(currentScroll + eventDelta, scrollWidth - ulWidth))
|
||||||
|
scrollState.value.translateX = -newScroll
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(route, () => {
|
||||||
|
addTags()
|
||||||
|
nextTick(() => {
|
||||||
|
setTransition()
|
||||||
|
autoPositionTab()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// 监听右键菜单可见性
|
|
||||||
watch(visible, (value) => {
|
watch(visible, (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
document.body.addEventListener('click', closeMenu)
|
document.body.addEventListener('click', closeMenu)
|
||||||
@@ -219,8 +283,10 @@ watch(visible, (value) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
addTags()
|
addTags()
|
||||||
initTags()
|
nextTick(() => {
|
||||||
container = document.querySelector('.tags-view-container')
|
setTransition()
|
||||||
|
autoPositionTab()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -228,152 +294,125 @@ onMounted(() => {
|
|||||||
.tags-view-container {
|
.tags-view-container {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--el-fill-color-blank);
|
background: #fff;
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
border-bottom: 1px solid #d8dce5;
|
||||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 9;
|
|
||||||
|
|
||||||
.tags-view-wrapper {
|
.tags-view-wrapper {
|
||||||
white-space: nowrap;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
white-space: nowrap;
|
||||||
|
|
||||||
:deep(.el-scrollbar__wrap) {
|
.scroll-content {
|
||||||
height: 34px;
|
display: inline-block;
|
||||||
overflow-x: auto;
|
white-space: nowrap;
|
||||||
scrollbar-width: thin;
|
will-change: transform;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
.tags-view-item {
|
||||||
height: 4px;
|
display: inline-flex;
|
||||||
}
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
&::-webkit-scrollbar-thumb {
|
cursor: pointer;
|
||||||
background-color: var(--el-border-color-darker);
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
border: 1px solid #d8dce5;
|
||||||
|
color: #495060;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&:first-of-type {
|
||||||
background-color: transparent;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-scrollbar__bar.is-horizontal) {
|
&:hover {
|
||||||
height: 0;
|
color: #409eff;
|
||||||
|
background: #ecf5ff;
|
||||||
|
border-color: #b3d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #409eff;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
background: #fff;
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
transform-origin: 100% 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-view-item {
|
.contextmenu {
|
||||||
display: inline-flex;
|
margin: 0;
|
||||||
position: relative;
|
background: #fff;
|
||||||
align-items: center;
|
z-index: 3000;
|
||||||
cursor: pointer;
|
position: absolute;
|
||||||
height: 26px;
|
list-style-type: none;
|
||||||
line-height: 26px;
|
padding: 5px 0;
|
||||||
border: 1px solid var(--el-border-color-light);
|
border-radius: 4px;
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
background: var(--el-fill-color-blank);
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-left: 5px;
|
font-weight: 400;
|
||||||
margin-top: 4px;
|
color: #333;
|
||||||
border-radius: 3px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
min-width: 100px;
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:first-of-type {
|
li {
|
||||||
margin-left: 10px;
|
margin: 0;
|
||||||
}
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
&:last-of-type {
|
display: flex;
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-fill-color-light);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
border-color: var(--el-color-primary-light-7);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: var(--el-color-primary);
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 500;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 8px rgba(var(--el-color-primary-rgb), 0.3);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
background: #fff;
|
|
||||||
display: inline-block;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: relative;
|
|
||||||
margin-right: 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon-close {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 8px;
|
||||||
border-radius: 50%;
|
transition: all 0.3s;
|
||||||
text-align: center;
|
|
||||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
||||||
transform-origin: 100% 50%;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-left: 6px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
line-height: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
flex-shrink: 0;
|
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #fff;
|
background: #ecf5ff;
|
||||||
color: var(--el-color-danger);
|
color: #409eff;
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.contextmenu {
|
.el-icon {
|
||||||
margin: 0;
|
font-size: 14px;
|
||||||
background: var(--el-bg-color);
|
}
|
||||||
z-index: 3000;
|
|
||||||
position: absolute;
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 5px 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
box-shadow: var(--el-box-shadow-light);
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,311 +1,187 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="userbar-container">
|
<div class="navbar-right">
|
||||||
<!-- 全屏切换 -->
|
<div class="right-item">
|
||||||
<el-tooltip content="全屏" placement="bottom">
|
<el-tooltip content="全屏" placement="bottom">
|
||||||
<div class="icon-btn" @click="toggleFullScreen">
|
<el-icon class="icon-btn" @click="toggleFullScreen">
|
||||||
<el-icon v-if="!isFullScreen">
|
<component :is="isFullscreen ? 'ElIconFullScreen' : 'ElIconAim'" />
|
||||||
<FullScreen />
|
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<el-icon v-else>
|
</el-tooltip>
|
||||||
<Aim />
|
</div>
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
<!-- 语言切换 -->
|
<div class="right-item">
|
||||||
<el-dropdown trigger="click" @command="handleLanguageChange">
|
<el-tooltip content="刷新" placement="bottom">
|
||||||
<div class="icon-btn">
|
<el-icon class="icon-btn" @click="refreshPage">
|
||||||
<el-icon>
|
<component :is="'ElIconRefresh'" />
|
||||||
<Location />
|
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</el-tooltip>
|
||||||
<template #dropdown>
|
</div>
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item :disabled="currentLanguage === 'zh-CN'" command="zh-CN"> 简体中文 </el-dropdown-item>
|
|
||||||
<el-dropdown-item :disabled="currentLanguage === 'en-US'" command="en-US"> English </el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
|
|
||||||
<!-- 主题设置 -->
|
<div class="right-item">
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click" @command="handleCommand">
|
||||||
<div class="icon-btn">
|
<span class="el-dropdown-link">
|
||||||
<el-icon>
|
<el-avatar :size="32" :src="userInfo.avatar || ''">
|
||||||
<Sunny />
|
<el-icon><component :is="'ElIconUser'" /></el-icon>
|
||||||
</el-icon>
|
</el-avatar>
|
||||||
</div>
|
<span class="username">{{ userInfo.username || '用户' }}</span>
|
||||||
<template #dropdown>
|
<el-icon class="el-icon--right"><component :is="'ElIconArrowDown'" /></el-icon>
|
||||||
<el-dropdown-menu>
|
</span>
|
||||||
<el-dropdown-item @click="setTheme('light')">
|
<template #dropdown>
|
||||||
<el-icon>
|
<el-dropdown-menu>
|
||||||
<Sunny />
|
<el-dropdown-item command="profile">
|
||||||
</el-icon>
|
<el-icon><component :is="'ElIconUser'" /></el-icon>
|
||||||
浅色主题
|
个人中心
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click="setTheme('dark')">
|
<el-dropdown-item command="settings">
|
||||||
<el-icon>
|
<el-icon><component :is="'ElIconSetting'" /></el-icon>
|
||||||
<Moon />
|
设置
|
||||||
</el-icon>
|
</el-dropdown-item>
|
||||||
深色主题
|
<el-dropdown-item divided command="logout">
|
||||||
</el-dropdown-item>
|
<el-icon><component :is="'ElIconSwitchButton'" /></el-icon>
|
||||||
</el-dropdown-menu>
|
退出登录
|
||||||
</template>
|
</el-dropdown-item>
|
||||||
</el-dropdown>
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
<!-- 消息通知 -->
|
</el-dropdown>
|
||||||
<el-tooltip content="消息" placement="bottom">
|
</div>
|
||||||
<div class="icon-btn">
|
|
||||||
<el-badge :value="messageCount" :hidden="messageCount === 0" :max="99">
|
|
||||||
<el-icon>
|
|
||||||
<Bell />
|
|
||||||
</el-icon>
|
|
||||||
</el-badge>
|
|
||||||
</div>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-dropdown trigger="click" @command="handleCommand">
|
|
||||||
<div class="avatar-wrapper">
|
|
||||||
<el-avatar v-if="userStore.userInfo?.avatar" :src="userStore.userInfo.avatar" />
|
|
||||||
<el-avatar v-else>
|
|
||||||
<UserFilled />
|
|
||||||
</el-avatar>
|
|
||||||
<span class="username">{{ userStore.userInfo?.username || 'Admin' }}</span>
|
|
||||||
<el-icon class="el-icon-caret-bottom">
|
|
||||||
<ArrowDown />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item command="home">
|
|
||||||
<el-icon>
|
|
||||||
<HomeFilled />
|
|
||||||
</el-icon>
|
|
||||||
首页
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item command="profile">
|
|
||||||
<el-icon>
|
|
||||||
<User />
|
|
||||||
</el-icon>
|
|
||||||
个人中心
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item divided command="settings">
|
|
||||||
<el-icon>
|
|
||||||
<Setting />
|
|
||||||
</el-icon>
|
|
||||||
系统设置
|
|
||||||
</el-dropdown-item>
|
|
||||||
<el-dropdown-item divided command="logout">
|
|
||||||
<el-icon>
|
|
||||||
<SwitchButton />
|
|
||||||
</el-icon>
|
|
||||||
退出登录
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
import { useUserStore } from '../../stores/modules/user'
|
||||||
import { ArrowDown, HomeFilled, User, Setting, SwitchButton, FullScreen, Aim, Location, Sunny, Moon, Bell, UserFilled } from '@element-plus/icons-vue'
|
import { useLayoutStore } from '../../stores/modules/layout'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useI18nStore } from '@/stores/modules/i18n'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LayoutUserbar',
|
name: 'UserBar',
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const i18nStore = useI18nStore()
|
const layoutStore = useLayoutStore()
|
||||||
|
|
||||||
// 全屏状态
|
const isFullscreen = ref(false)
|
||||||
const isFullScreen = ref(false)
|
const userInfo = ref({
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
})
|
||||||
|
|
||||||
// 消息数量
|
// 切换全屏
|
||||||
const messageCount = ref(0)
|
const toggleFullScreen = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen()
|
||||||
|
isFullscreen.value = true
|
||||||
|
} else {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
isFullscreen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 当前语言
|
// 刷新页面
|
||||||
const currentLanguage = computed(() => i18nStore.locale)
|
const refreshPage = () => {
|
||||||
|
layoutStore.refreshTag()
|
||||||
|
router.replace({
|
||||||
|
path: '/redirect' + router.currentRoute.value.fullPath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 处理下拉菜单命令
|
// 处理下拉菜单命令
|
||||||
const handleCommand = (command) => {
|
const handleCommand = (command) => {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'home':
|
|
||||||
router.push('/')
|
|
||||||
break
|
|
||||||
case 'profile':
|
case 'profile':
|
||||||
router.push('/profile')
|
router.push('/ucenter/profile')
|
||||||
break
|
break
|
||||||
case 'settings':
|
case 'settings':
|
||||||
router.push('/settings')
|
router.push('/ucenter/settings')
|
||||||
break
|
break
|
||||||
case 'logout':
|
case 'logout':
|
||||||
handleLogout()
|
handleLogout()
|
||||||
break
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
userStore.logout()
|
||||||
confirmButtonText: '确定',
|
ElMessage.success('退出登录成功')
|
||||||
cancelButtonText: '取消',
|
router.push('/login')
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
userStore.logout()
|
|
||||||
router.push('/login')
|
|
||||||
ElMessage.success('退出成功')
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// 取消退出
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换全屏
|
// 获取用户信息
|
||||||
const toggleFullScreen = () => {
|
const getUserInfo = () => {
|
||||||
if (!document.fullscreenElement) {
|
const info = userStore.userInfo
|
||||||
document.documentElement.requestFullscreen()
|
if (info) {
|
||||||
isFullScreen.value = true
|
userInfo.value = {
|
||||||
} else {
|
username: info.username || info.nickName || '用户',
|
||||||
if (document.exitFullscreen) {
|
avatar: info.avatar || '',
|
||||||
document.exitFullscreen()
|
|
||||||
isFullScreen.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听全屏变化
|
// 监听全屏变化
|
||||||
const handleFullScreenChange = () => {
|
const handleFullscreenChange = () => {
|
||||||
isFullScreen.value = !!document.fullscreenElement
|
isFullscreen.value = !!document.fullscreenElement
|
||||||
}
|
|
||||||
|
|
||||||
// 语言切换
|
|
||||||
const handleLanguageChange = (lang) => {
|
|
||||||
i18nStore.setLocale(lang)
|
|
||||||
ElMessage.success('语言切换成功')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置主题
|
|
||||||
const setTheme = (theme) => {
|
|
||||||
if (theme === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
}
|
|
||||||
ElMessage.success('主题切换成功')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('fullscreenchange', handleFullScreenChange)
|
getUserInfo()
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('fullscreenchange', handleFullScreenChange)
|
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.userbar-container {
|
.navbar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
height: 100%;
|
||||||
|
|
||||||
.avatar-wrapper {
|
.right-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
padding: 0 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&:hover {
|
.icon-btn {
|
||||||
background-color: var(--el-fill-color-light);
|
font-size: 20px;
|
||||||
transform: translateY(-1px);
|
color: #5a5e66;
|
||||||
}
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
.username {
|
&:hover {
|
||||||
font-size: 14px;
|
color: var(--primary-color);
|
||||||
font-weight: 500;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
max-width: 100px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon-caret-bottom {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
.avatar-wrapper:hover & {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-fill-color-light);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
.el-dropdown-link {
|
||||||
transform: translateY(0);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #5a5e66;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon--right {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-badge__content) {
|
|
||||||
transform: translateY(-8px) translateX(8px);
|
|
||||||
border: 2px solid var(--el-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-badge__content.is-fixed) {
|
|
||||||
transform: translateY(-8px) translateX(8px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下拉菜单样式优化
|
|
||||||
:deep(.el-dropdown-menu__item) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--el-fill-color-light);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,13 @@
|
|||||||
<img :src="logo" alt="logo" />
|
<img :src="logo" alt="logo" />
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-list">
|
<div class="menu-list">
|
||||||
<div v-for="menu in parentMenus" :key="menu.path" class="menu-item" :class="{ active: selectedParentMenu?.path === menu.path }" @click="handleParentMenuClick(menu)">
|
<div
|
||||||
|
v-for="menu in parentMenus"
|
||||||
|
:key="menu.path"
|
||||||
|
class="menu-item"
|
||||||
|
:class="{ active: selectedParentMenu?.path === menu.path }"
|
||||||
|
@click="handleParentMenuClick(menu)"
|
||||||
|
>
|
||||||
<el-icon v-if="menu.meta?.icon">
|
<el-icon v-if="menu.meta?.icon">
|
||||||
<component :is="menu.meta.icon" />
|
<component :is="menu.meta.icon" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
@@ -19,13 +25,15 @@
|
|||||||
|
|
||||||
<!-- 二级菜单栏 -->
|
<!-- 二级菜单栏 -->
|
||||||
<el-aside :width="sidebarCollapsed ? '60px' : '220px'" class="second-sidebar">
|
<el-aside :width="sidebarCollapsed ? '60px' : '220px'" class="second-sidebar">
|
||||||
<div v-if="!sidebarCollapsed" class="logo logo-only">
|
<div v-if="!sidebarCollapsed && selectedParentMenu" class="second-sidebar-header">
|
||||||
<img v-if="logo" :src="logo" alt="logo" />
|
<el-icon v-if="selectedParentMenu.meta?.icon" class="menu-icon">
|
||||||
<span class="logo-text hidden">{{ logoText }}</span>
|
<component :is="selectedParentMenu.meta.icon" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="menu-title">{{ selectedParentMenu.meta?.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="menu-scrollbar">
|
<el-menu :default-active="activeMenu" :collapse="sidebarCollapsed" :collapse-transition="false">
|
||||||
<menu-component />
|
<layout-menu-item :menu-list="childMenus" />
|
||||||
</el-scrollbar>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
@@ -35,14 +43,14 @@
|
|||||||
<el-icon class="collapse-icon" @click="toggleSidebar">
|
<el-icon class="collapse-icon" @click="toggleSidebar">
|
||||||
<component :is="sidebarCollapsed ? 'ElIconExpand' : 'ElIconFold'" />
|
<component :is="sidebarCollapsed ? 'ElIconExpand' : 'ElIconFold'" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<breadcrumb v-if="showBreadcrumb" />
|
<layout-breadcrumb v-if="showBreadcrumb" />
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<userbar />
|
<userbar />
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
<tags v-if="showTags" />
|
||||||
<el-main class="app-main">
|
<el-main class="app-main">
|
||||||
<tags v-if="showTags" />
|
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<keep-alive :include="cachedViews">
|
<keep-alive :include="cachedViews">
|
||||||
<component v-if="!route.meta.link" :is="Component" :key="refreshKey" />
|
<component v-if="!route.meta.link" :is="Component" :key="refreshKey" />
|
||||||
@@ -60,9 +68,9 @@
|
|||||||
<img v-if="logo" :src="logo" alt="logo" />
|
<img v-if="logo" :src="logo" alt="logo" />
|
||||||
<span class="logo-text">{{ logoText }}</span>
|
<span class="logo-text">{{ logoText }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="menu-scrollbar">
|
<el-menu :default-active="activeMenu" :collapse="sidebarCollapsed" :collapse-transition="false">
|
||||||
<menu-component />
|
<layout-menu-item :menu-list="allMenus" />
|
||||||
</el-scrollbar>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container class="main-container">
|
<el-container class="main-container">
|
||||||
<el-header class="app-header">
|
<el-header class="app-header">
|
||||||
@@ -70,13 +78,13 @@
|
|||||||
<el-icon class="collapse-icon" @click="toggleSidebar">
|
<el-icon class="collapse-icon" @click="toggleSidebar">
|
||||||
<component :is="sidebarCollapsed ? 'ElIconExpand' : 'ElIconFold'" />
|
<component :is="sidebarCollapsed ? 'ElIconExpand' : 'ElIconFold'" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<breadcrumb v-if="showBreadcrumb" />
|
<layout-breadcrumb v-if="showBreadcrumb" />
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<tags v-if="showTags" />
|
|
||||||
<userbar />
|
<userbar />
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
<tags v-if="showTags" />
|
||||||
<el-main class="app-main">
|
<el-main class="app-main">
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<keep-alive :include="cachedViews">
|
<keep-alive :include="cachedViews">
|
||||||
@@ -97,14 +105,16 @@
|
|||||||
<img v-if="logo" :src="logo" alt="logo" />
|
<img v-if="logo" :src="logo" alt="logo" />
|
||||||
<span class="logo-text">{{ logoText }}</span>
|
<span class="logo-text">{{ logoText }}</span>
|
||||||
</div>
|
</div>
|
||||||
<menu-component mode="horizontal" />
|
<el-menu :default-active="activeMenu" mode="horizontal" :ellipsis="false">
|
||||||
|
<layout-menu-item :menu-list="allMenus" />
|
||||||
|
</el-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<userbar />
|
<userbar />
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
<tags v-if="showTags" />
|
||||||
<el-main class="app-main">
|
<el-main class="app-main">
|
||||||
<tags v-if="showTags" />
|
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<keep-alive :include="cachedViews">
|
<keep-alive :include="cachedViews">
|
||||||
<component v-if="!route.meta.link" :is="Component" :key="refreshKey" />
|
<component v-if="!route.meta.link" :is="Component" :key="refreshKey" />
|
||||||
@@ -117,60 +127,87 @@
|
|||||||
</el-container>
|
</el-container>
|
||||||
|
|
||||||
<!-- 设置组件 -->
|
<!-- 设置组件 -->
|
||||||
<setting />
|
<layout-setting />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
import { useUserStore } from '../stores/modules/user'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useLayoutStore } from '../stores/modules/layout'
|
||||||
import MenuComponent from './components/menu.vue'
|
import LayoutMenuItem from './components/menu-item.vue'
|
||||||
import Breadcrumb from './components/breadcrumb.vue'
|
import LayoutBreadcrumb from './components/breadcrumb.vue'
|
||||||
import Tags from './components/tags.vue'
|
import Tags from './components/tags.vue'
|
||||||
import Userbar from './components/userbar.vue'
|
import Userbar from './components/userbar.vue'
|
||||||
import Setting from './components/setting.vue'
|
import LayoutSetting from './components/setting.vue'
|
||||||
import config from '@/config'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AppLayouts',
|
name: 'AppLayouts',
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const layoutStore = useLayoutStore()
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const layoutStore = useLayoutStore()
|
||||||
|
|
||||||
// Logo 配置
|
// Logo配置
|
||||||
const logo = config.LOGO
|
const logo = computed(() => {
|
||||||
const logoText = config.APP_NAME
|
return new URL('../assets/logo.png', import.meta.url).href
|
||||||
|
})
|
||||||
|
|
||||||
// 布局相关状态
|
const logoText = computed(() => {
|
||||||
|
return import.meta.env.VITE_APP_TITLE || 'Admin'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 布局相关
|
||||||
const layoutMode = computed(() => layoutStore.layoutMode)
|
const layoutMode = computed(() => layoutStore.layoutMode)
|
||||||
const sidebarCollapsed = computed(() => layoutStore.sidebarCollapsed)
|
const sidebarCollapsed = computed(() => layoutStore.sidebarCollapsed)
|
||||||
const showTags = computed(() => layoutStore.showTags)
|
const showTags = computed(() => layoutStore.showTags)
|
||||||
const showBreadcrumb = computed(() => layoutStore.showBreadcrumb)
|
const showBreadcrumb = computed(() => layoutStore.showBreadcrumb)
|
||||||
const selectedParentMenu = computed(() => layoutStore.selectedParentMenu)
|
|
||||||
const refreshKey = computed(() => layoutStore.refreshKey)
|
const refreshKey = computed(() => layoutStore.refreshKey)
|
||||||
const viewTags = computed(() => layoutStore.viewTags)
|
|
||||||
|
|
||||||
// 布局样式类
|
// 菜单数据
|
||||||
const layoutClass = computed(() => {
|
const allMenus = computed(() => {
|
||||||
return {
|
return userStore.getMenu() || []
|
||||||
[`layout-${layoutMode.value}`]: true,
|
})
|
||||||
'sidebar-collapsed': sidebarCollapsed.value,
|
|
||||||
|
const parentMenus = computed(() => {
|
||||||
|
return allMenus.value.filter((menu) => !menu.meta?.hidden)
|
||||||
|
})
|
||||||
|
|
||||||
|
const childMenus = computed(() => {
|
||||||
|
if (!selectedParentMenu.value) {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
return selectedParentMenu.value.children || []
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedParentMenu = computed({
|
||||||
|
get: () => layoutStore.selectedParentMenu,
|
||||||
|
set: (value) => layoutStore.setSelectedParentMenu(value),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前激活的菜单
|
||||||
|
const activeMenu = computed(() => {
|
||||||
|
const { meta, path } = route
|
||||||
|
if (meta.activeMenu) {
|
||||||
|
return meta.activeMenu
|
||||||
|
}
|
||||||
|
return path
|
||||||
})
|
})
|
||||||
|
|
||||||
// 缓存的视图
|
// 缓存的视图
|
||||||
const cachedViews = computed(() => {
|
const cachedViews = computed(() => {
|
||||||
return viewTags.value.filter((tag) => tag.meta?.keepAlive).map((tag) => tag.name)
|
return layoutStore.viewTags.map((tag) => tag.name).filter((name) => name)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 父级菜单(一级菜单)
|
// 布局样式类
|
||||||
const parentMenus = computed(() => {
|
const layoutClass = computed(() => {
|
||||||
const menus = userStore.getMenu() || []
|
return {
|
||||||
return menus.filter((menu) => !menu.meta?.hidden && menu.children && menu.children.length > 0)
|
'layout-default': layoutMode.value === 'default',
|
||||||
|
'layout-menu': layoutMode.value === 'menu',
|
||||||
|
'layout-top': layoutMode.value === 'top',
|
||||||
|
'sidebar-collapsed': sidebarCollapsed.value,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 切换侧边栏
|
// 切换侧边栏
|
||||||
@@ -178,380 +215,254 @@ const toggleSidebar = () => {
|
|||||||
layoutStore.toggleSidebar()
|
layoutStore.toggleSidebar()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理父菜单点击
|
// 处理一级菜单点击
|
||||||
const handleParentMenuClick = (menu) => {
|
const handleParentMenuClick = (menu) => {
|
||||||
// 如果点击的是当前选中的父菜单,不重复操作
|
selectedParentMenu.value = menu
|
||||||
if (selectedParentMenu.value?.path === menu.path) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置选中的父菜单
|
|
||||||
layoutStore.setSelectedParentMenu(menu)
|
|
||||||
|
|
||||||
// 如果父菜单有 redirect,跳转
|
|
||||||
if (menu.redirect) {
|
|
||||||
router.push(menu.redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有子菜单,跳转到第一个子菜单
|
|
||||||
if (menu.children && menu.children.length > 0) {
|
|
||||||
const firstChild = menu.children.find((child) => !child.meta?.hidden) || menu.children[0]
|
|
||||||
if (firstChild) {
|
|
||||||
router.push(firstChild.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听路由变化,自动更新选中的父菜单
|
// 监听路由变化,自动选择父菜单
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
() => {
|
() => {
|
||||||
if (layoutMode.value === 'default') {
|
if (layoutMode.value === 'default') {
|
||||||
const menus = userStore.getMenu() || []
|
const path = route.path
|
||||||
for (const parentMenu of menus) {
|
const parent = parentMenus.value.find((menu) => {
|
||||||
if (parentMenu.children && parentMenu.children.some((child) => route.path.startsWith(child.path))) {
|
if (menu.path === path || path.startsWith(menu.path + '/')) {
|
||||||
layoutStore.setSelectedParentMenu(parentMenu)
|
return true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
if (menu.children) {
|
||||||
|
return menu.children.some((child) => child.path === path || path.startsWith(child.path + '/'))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (parent && (!selectedParentMenu.value || !path.startsWith(selectedParentMenu.value.path + '/'))) {
|
||||||
|
selectedParentMenu.value = parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新视图标签
|
|
||||||
if (route.name && !route.meta?.hidden) {
|
|
||||||
layoutStore.updateViewTags({
|
|
||||||
name: route.name,
|
|
||||||
path: route.path,
|
|
||||||
fullPath: route.fullPath,
|
|
||||||
title: route.meta?.title,
|
|
||||||
meta: route.meta,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.app-wrapper {
|
.app-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
// 通用侧边栏样式
|
&.layout-default {
|
||||||
.el-aside {
|
|
||||||
transition: width 0.28s;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--el-menu-bg-color);
|
|
||||||
border-right: 1px solid var(--el-border-color-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logo 样式
|
.first-sidebar {
|
||||||
.logo {
|
width: 60px;
|
||||||
height: 60px;
|
height: 100%;
|
||||||
display: flex;
|
background: #001529;
|
||||||
align-items: center;
|
overflow-x: hidden;
|
||||||
justify-content: center;
|
|
||||||
padding: 0 20px;
|
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 32px;
|
|
||||||
max-width: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text {
|
|
||||||
margin-left: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 双栏布局只显示logo,隐藏名称
|
|
||||||
&.logo-only .logo-text.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.logo-only {
|
|
||||||
justify-content: center;
|
|
||||||
img {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-mini {
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text-mini {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header 样式
|
|
||||||
.el-header {
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 20px;
|
|
||||||
background-color: var(--el-bg-color);
|
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
|
||||||
|
|
||||||
.header-left,
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
transition: color 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main 样式
|
|
||||||
.el-main {
|
|
||||||
padding: 0;
|
|
||||||
background-color: var(--el-bg-color-page);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iframe-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认布局样式
|
|
||||||
.layout-default {
|
|
||||||
.first-sidebar {
|
|
||||||
.menu-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px 0;
|
|
||||||
|
|
||||||
.menu-item {
|
.logo-mini {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
background: #002140;
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
img {
|
||||||
background-color: var(--el-fill-color-light);
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
.menu-item {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
background: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-sidebar {
|
||||||
|
background: #fff;
|
||||||
|
border-right: 1px solid #e6e6e6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: width 0.3s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.second-sidebar-header {
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
background: #fafafa;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
.menu-title {
|
||||||
color: var(--el-color-primary);
|
font-size: 15px;
|
||||||
background-color: var(--el-color-primary-light-9);
|
font-weight: 500;
|
||||||
}
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
.el-icon {
|
overflow: hidden;
|
||||||
font-size: 20px;
|
text-overflow: ellipsis;
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-text {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.second-sidebar {
|
&.layout-menu {
|
||||||
.menu-scrollbar {
|
display: flex;
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menu 布局样式
|
.menu-sidebar {
|
||||||
.layout-menu {
|
background: #fff;
|
||||||
.menu-sidebar {
|
border-right: 1px solid #e6e6e6;
|
||||||
.menu-scrollbar {
|
overflow-x: hidden;
|
||||||
flex: 1;
|
overflow-y: auto;
|
||||||
overflow: hidden;
|
transition: width 0.3s;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top 布局样式
|
|
||||||
.layout-top {
|
|
||||||
.top-header {
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
border: none;
|
height: 50px;
|
||||||
padding: 0;
|
display: flex;
|
||||||
height: 60px;
|
align-items: center;
|
||||||
}
|
padding: 0 20px;
|
||||||
}
|
border-bottom: 1px solid #e6e6e6;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 侧边栏折叠状态
|
img {
|
||||||
.sidebar-collapsed {
|
width: 32px;
|
||||||
.logo-text {
|
height: 32px;
|
||||||
display: none !important;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式设计
|
.logo-text {
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
// 小屏幕优化
|
|
||||||
.layout-default {
|
|
||||||
.first-sidebar {
|
|
||||||
width: 50px !important;
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
height: 45px;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menu-text {
|
&.layout-top {
|
||||||
font-size: 10px;
|
.top-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 50px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 40px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-mini img {
|
.el-menu {
|
||||||
height: 28px;
|
border-bottom: none;
|
||||||
width: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.second-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
left: 50px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-menu {
|
|
||||||
.menu-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--el-bg-color);
|
|
||||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header 优化
|
|
||||||
.el-header {
|
|
||||||
padding: 0 12px;
|
|
||||||
|
|
||||||
.header-left,
|
|
||||||
.header-right {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.breadcrumb) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tags 优化
|
|
||||||
.tags-view-item {
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
// 超小屏幕优化
|
|
||||||
.layout-default {
|
|
||||||
.first-sidebar {
|
|
||||||
width: 45px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.second-sidebar {
|
|
||||||
left: 45px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-menu {
|
|
||||||
.menu-sidebar {
|
|
||||||
width: 200px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header 优化
|
|
||||||
.el-header {
|
|
||||||
padding: 0 8px;
|
|
||||||
|
|
||||||
.header-left,
|
|
||||||
.header-right {
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-wrapper {
|
|
||||||
padding: 4px 8px;
|
|
||||||
|
|
||||||
.username {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo 优化
|
.main-container {
|
||||||
.logo {
|
flex: 1;
|
||||||
img {
|
display: flex;
|
||||||
height: 28px;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
height: 50px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-text {
|
.app-main {
|
||||||
font-size: 16px;
|
flex: 1;
|
||||||
|
background: #f0f2f5;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.iframe-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,8 @@ const goHome = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '@/assets/style/auth.scss';
|
|
||||||
|
|
||||||
.not-found-container {
|
.not-found-container {
|
||||||
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Box } from '@element-plus/icons-vue'
|
import { Box } from '@element-plus/icons-vue'
|
||||||
import '@/assets/style/auth.scss'
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
description: {
|
description: {
|
||||||
@@ -59,9 +58,8 @@ const handleAction = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '@/assets/style/auth.scss';
|
|
||||||
|
|
||||||
.empty-container {
|
.empty-container {
|
||||||
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user