You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
419 lines
11 KiB
419 lines
11 KiB
7 months ago
|
<template>
|
||
|
<div class="root">
|
||
|
<div class="menu-box">
|
||
|
<div class="logo">
|
||
|
<img :src="logoIcon" alt="" class="logo-icon" />
|
||
|
<span class="logo-label">设备管理平台</span>
|
||
|
</div>
|
||
|
<el-scrollbar>
|
||
|
<el-menu :default-active="activeMenu" class="menu" @select="menuClick">
|
||
|
<template v-for="(menu, idx) in menuData" :key="idx">
|
||
|
<el-menu-item
|
||
|
:index="menu.path"
|
||
|
class="menu-item"
|
||
|
v-if="!menu?.children?.length"
|
||
|
>
|
||
|
<Icon :size="12" v-if="menu.meta.icon" :icon="menu.meta.icon" />
|
||
|
<span class="menu-label">{{ menu.meta.title }}</span>
|
||
|
</el-menu-item>
|
||
|
<el-sub-menu :index="menu.path" v-else>
|
||
|
<template #title>
|
||
|
<Icon :size="12" v-if="menu.meta.icon" :icon="menu.meta.icon" />
|
||
|
<span class="menu-label">{{ menu.meta.title }}</span>
|
||
|
</template>
|
||
|
<template v-for="(child, index) in menu.children" :key="index">
|
||
|
<el-menu-item
|
||
|
class="menu-item"
|
||
|
:index="`${menu.path}/${child.path}`"
|
||
|
v-if="!child?.children?.length"
|
||
|
>
|
||
|
<Icon :size="12" v-if="child.meta.icon" :icon="child.meta.icon" />
|
||
|
|
||
|
{{ child.meta.title }}
|
||
|
</el-menu-item>
|
||
|
|
||
|
<el-sub-menu :index="`${menu.path}/${child.path}`" v-else>
|
||
|
<template #title>
|
||
|
<Icon :size="12" v-if="child.meta.icon" :icon="child.meta.icon" />
|
||
|
|
||
|
{{ child.meta.title }}
|
||
|
</template>
|
||
|
<template
|
||
|
v-for="(subChild, subIndex) in child.children"
|
||
|
:key="subIndex"
|
||
|
>
|
||
|
<el-menu-item
|
||
|
class="menu-item"
|
||
|
:index="`${menu.path}/${child.path}/${subChild.path}`"
|
||
|
>
|
||
|
<Icon
|
||
|
:size="12"
|
||
|
v-if="subChild.meta.icon"
|
||
|
:icon="subChild.meta.icon"
|
||
|
/>
|
||
|
|
||
|
{{ subChild.meta.title }}
|
||
|
</el-menu-item>
|
||
|
</template>
|
||
|
</el-sub-menu>
|
||
|
</template>
|
||
|
</el-sub-menu>
|
||
|
</template>
|
||
|
</el-menu>
|
||
|
</el-scrollbar>
|
||
|
</div>
|
||
|
|
||
|
<div class="root-right">
|
||
|
<el-header class="header">
|
||
|
<div class="time">
|
||
|
{{ currentTime }}
|
||
|
</div>
|
||
|
<div class="right">
|
||
|
<el-dropdown
|
||
|
style="width: 100%; height: 100%"
|
||
|
@command="command => handleCommand(command)"
|
||
|
>
|
||
|
<div class="user-avatar">
|
||
|
<img :src="Avatar" height="32" />
|
||
|
<div class="user-name" :title="userName">{{ userName }}</div>
|
||
|
</div>
|
||
|
<template #dropdown>
|
||
|
<el-dropdown-menu>
|
||
|
<el-dropdown-item command="personal">个人中心</el-dropdown-item>
|
||
|
<el-dropdown-item command="theme">{{ themeText }}</el-dropdown-item>
|
||
|
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||
|
</el-dropdown-menu>
|
||
|
</template>
|
||
|
</el-dropdown>
|
||
|
</div>
|
||
|
</el-header>
|
||
|
<main class="layout-main">
|
||
|
<el-config-provider :locale="zhCn">
|
||
|
<RouterView />
|
||
|
</el-config-provider>
|
||
|
<!-- </div> -->
|
||
|
</main>
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script setup lang="ts">
|
||
|
import Icon from '@/components/dashboard/Icon/src/Icon.vue'
|
||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||
|
import { cloneDeep, debounce } from 'lodash'
|
||
|
import { RouterView, useRouter, useRoute } from 'vue-router'
|
||
|
import logoIcon from '@/assets/image/dashboard/common/icon_logo_pg.png'
|
||
|
import { usePermissionStore } from '@/stores/permission'
|
||
|
import dayjs from 'dayjs'
|
||
|
import { openSocket, closeSocket } from '@/pages/socket_server/index'
|
||
|
import { useUserStore } from '@/stores/user'
|
||
|
import { isResError } from '@/hooks/useMessage'
|
||
|
import Avatar from './avatar.png'
|
||
|
import { useI18n } from 'vue-i18n'
|
||
|
import { useTheme } from '@/utils/useTheme'
|
||
|
import { THEME_KEY, INIT_OPTIONS_KEY } from 'vue-echarts'
|
||
|
import { registerTheme } from 'echarts/core'
|
||
|
const { theme, toggle } = useTheme()
|
||
|
import { customDark, customLight } from '@/utils/dark'
|
||
|
registerTheme('dark', customDark)
|
||
|
registerTheme('light', customLight)
|
||
|
provide(THEME_KEY, theme)
|
||
|
provide(INIT_OPTIONS_KEY, {
|
||
|
devicePixelRatio: 2,
|
||
|
})
|
||
|
|
||
|
const route = useRoute()
|
||
|
|
||
|
const breadcrumbItems = computed(() => {
|
||
|
console.log(route.matched)
|
||
|
const matched = route.matched.filter(m => m.meta.title)
|
||
|
return matched.map(m => ({
|
||
|
name: m.meta.title,
|
||
|
path: m.path,
|
||
|
}))
|
||
|
})
|
||
|
|
||
|
function transformMenu(menu: any) {
|
||
|
const newMenu = []
|
||
|
|
||
|
function handleChildren(item: any) {
|
||
|
if (item.children && item.children.length === 1) {
|
||
|
const child = item.children[0]
|
||
|
item.path = `${item.path}${child.path ? '/' : ''}${child.path}`
|
||
|
item.name = child.name
|
||
|
item.meta = { ...item.meta, ...child.meta }
|
||
|
item.children = child.children || []
|
||
|
}
|
||
|
return item
|
||
|
}
|
||
|
|
||
|
for (const item of menu) {
|
||
|
const transformedItem = handleChildren(item)
|
||
|
if (!transformedItem.children?.length) {
|
||
|
delete transformedItem.children
|
||
|
}
|
||
|
newMenu.push(transformedItem)
|
||
|
}
|
||
|
return newMenu
|
||
|
}
|
||
|
|
||
|
const themeText = computed(() => (theme.value === 'dark' ? '亮色主题' : '暗色主题'))
|
||
|
|
||
|
const userStore = useUserStore()
|
||
|
|
||
|
const { t } = useI18n() // 国际化
|
||
|
const avatar = computed(() => userStore.user.avatar ?? '')
|
||
|
const userName = computed(() => userStore.user.nickname ?? 'Admin')
|
||
|
openSocket()
|
||
|
const { push, currentRoute } = useRouter()
|
||
|
|
||
|
const permissionStore = usePermissionStore()
|
||
|
const menuData = computed(() =>
|
||
|
transformMenu(filterHiddenRoutes(cloneDeep(permissionStore.getRouters)))
|
||
|
) as ComputedRef<any>
|
||
|
|
||
|
function filterHiddenRoutes(routes: any[]) {
|
||
|
return routes
|
||
|
.filter((route: any) => !route.meta?.hidden)
|
||
|
.map((route: any) => {
|
||
|
if (route.children) {
|
||
|
route.children = filterHiddenRoutes(route.children)
|
||
|
}
|
||
|
return route
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 当前时间
|
||
|
const currentTime = ref('')
|
||
|
const updateTime = () => {
|
||
|
currentTime.value = dayjs().format('YYYY-MM-DD HH:mm')
|
||
|
}
|
||
|
updateTime()
|
||
|
setInterval(updateTime, 5000)
|
||
|
|
||
|
//菜单相关
|
||
|
const activeIndex = ref('')
|
||
|
const isCollapse = ref(false)
|
||
|
const menuSwitch = () => {
|
||
|
const el = document.querySelector('.menu-box') as HTMLElement
|
||
|
if (el) {
|
||
|
isCollapse.value = !isCollapse.value
|
||
|
if (isCollapse.value) {
|
||
|
el.classList.add('collapsed')
|
||
|
} else {
|
||
|
el.classList.remove('collapsed')
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const menuClick = (index: string) => {
|
||
|
activeIndex.value = index
|
||
|
push(activeIndex.value)
|
||
|
}
|
||
|
const activeMenu = computed(() => {
|
||
|
const { path } = unref(currentRoute)
|
||
|
return path
|
||
|
})
|
||
|
|
||
|
function handleCommand(command: string) {
|
||
|
if (command === 'logout') {
|
||
|
loginOut()
|
||
|
}
|
||
|
if (command === 'personal') {
|
||
|
push('/user/profile')
|
||
|
}
|
||
|
if (command === 'theme') {
|
||
|
toggle()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function loginOut() {
|
||
|
try {
|
||
|
await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
|
||
|
confirmButtonText: t('common.ok'),
|
||
|
cancelButtonText: t('common.cancel'),
|
||
|
type: 'warning',
|
||
|
})
|
||
|
await userStore.loginout()
|
||
|
push('/login')
|
||
|
} catch {}
|
||
|
}
|
||
|
|
||
|
onBeforeUnmount(() => {
|
||
|
closeSocket()
|
||
|
})
|
||
|
</script>
|
||
|
|
||
|
<style scoped lang="scss">
|
||
|
$header-height: 48px;
|
||
|
.root {
|
||
|
width: 100%;
|
||
|
height: 100%;
|
||
|
display: flex;
|
||
|
|
||
|
.menu-box {
|
||
|
max-width: 216px;
|
||
|
height: 100%;
|
||
|
position: relative;
|
||
|
transition: width 0.3s ease;
|
||
|
overflow: hidden;
|
||
|
background-color: var(--layout-menu-bg);
|
||
|
@include ResponsiveW(216);
|
||
|
|
||
|
.logo {
|
||
|
height: 48px;
|
||
|
width: 100%;
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
box-sizing: border-box;
|
||
|
font-family: FZZYJW--GB1-0;
|
||
|
background: var(--layout-menu-bg);
|
||
|
justify-content: center;
|
||
|
column-gap: 6px;
|
||
|
font-size: 16px;
|
||
|
color: #fff;
|
||
|
font-weight: 400;
|
||
|
|
||
|
.logo-icon {
|
||
|
width: 27px;
|
||
|
height: 27px;
|
||
|
}
|
||
|
|
||
|
.logo-label {
|
||
|
font-size: 16px;
|
||
|
line-height: 22px;
|
||
|
font-weight: 400;
|
||
|
}
|
||
|
|
||
|
.icon {
|
||
|
width: 20px;
|
||
|
height: 20px;
|
||
|
}
|
||
|
}
|
||
|
:deep(.el-menu) {
|
||
|
background-color: var(--layout-menu-bg);
|
||
|
border-right: none;
|
||
|
}
|
||
|
.menu {
|
||
|
height: calc(100% - 48px);
|
||
|
width: 100%;
|
||
|
overflow: auto;
|
||
|
background-color: var(--layout-menu-bg);
|
||
|
:deep(.el-menu-item:hover),
|
||
|
:deep(.el-sub-menu__title:hover) {
|
||
|
background: #272b35;
|
||
|
}
|
||
|
|
||
|
:deep(.el-menu-item),
|
||
|
:deep(.el-sub-menu__title) {
|
||
|
font-family: Alibaba-PuHuiTi-R;
|
||
|
font-size: 14px;
|
||
|
color: #fff;
|
||
|
line-height: 22px;
|
||
|
font-weight: 400;
|
||
|
height: 40px;
|
||
|
border-left: 3px solid transparent;
|
||
|
}
|
||
|
:deep(.el-menu-item.is-active) {
|
||
|
background-color: #2c342c;
|
||
|
border-left: 3px solid #619925;
|
||
|
}
|
||
|
.mr-5px {
|
||
|
margin-right: 5px;
|
||
|
font-size: 26px;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
.menu-box.collapsed {
|
||
|
width: 0;
|
||
|
}
|
||
|
|
||
|
.root-right {
|
||
|
flex: 1;
|
||
|
overflow: hidden;
|
||
|
}
|
||
|
|
||
|
.el-menu--collapse {
|
||
|
overflow: hidden;
|
||
|
width: 0;
|
||
|
}
|
||
|
|
||
|
.header {
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
justify-content: start;
|
||
|
background: var(--layout-header-bg);
|
||
|
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.12);
|
||
|
height: $header-height;
|
||
|
|
||
|
.btn {
|
||
|
--el-button-bg-color: transparent;
|
||
|
--el-button-border-color: transparent;
|
||
|
--el-button-hover-bg-color: transparent;
|
||
|
--el-button-hover-border-color: transparent;
|
||
|
padding: 0;
|
||
|
}
|
||
|
|
||
|
.switch {
|
||
|
width: 24px;
|
||
|
height: 24px;
|
||
|
padding: 3px;
|
||
|
}
|
||
|
|
||
|
.time {
|
||
|
margin-left: 19px;
|
||
|
font-family: Alibaba-PuHuiTi-R;
|
||
|
font-size: 14px;
|
||
|
color: var(--text-color);
|
||
|
font-weight: 400;
|
||
|
}
|
||
|
|
||
|
.right {
|
||
|
margin-left: auto;
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
|
||
|
.user-avatar {
|
||
|
display: flex;
|
||
|
outline: none;
|
||
|
align-items: center;
|
||
|
cursor: pointer;
|
||
|
.user-name {
|
||
|
font-size: 14px;
|
||
|
color: var(--text-color);
|
||
|
font-weight: 400;
|
||
|
margin-left: 4px;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
.icon {
|
||
|
width: 24px;
|
||
|
height: 24px;
|
||
|
padding: 3px;
|
||
|
// margin-left: 24px;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
.layout-main {
|
||
|
height: calc(100% - $header-height - 28px);
|
||
|
width: calc(100% - 28px);
|
||
|
min-width: 800px;
|
||
|
padding: 14px;
|
||
|
background: var(--bg);
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
.layout-breadcrumb {
|
||
|
height: 20px;
|
||
|
margin-bottom: 14px;
|
||
|
}
|
||
|
.layout-view {
|
||
|
flex: 1;
|
||
|
background: var(--bg);
|
||
|
border-radius: 4px;
|
||
|
}
|
||
|
// overflow: hidden;
|
||
|
}
|
||
|
}
|
||
|
</style>
|