You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

375 lines
8.4 KiB
Vue

<script setup lang="ts">
import type { CSSProperties } from 'vue'
import type { RouteLocationNormalized } from 'vue-router/auto'
import Logo from '../logo/index.vue'
import SiderTrigger from '../trigger/sider-trigger.vue'
import MixSubMenu from './mix-sub-menu.vue'
import { createNamespace, listenerRouteChange } from '~/utils'
import { MixSidebarTriggerEnum, NavBarModeEnum } from '~/constants'
const props = defineProps({
mixSidebarWidth: { type: Number, default: 40 },
})
const title = import.meta.env.VITE_APP_TITLE
const { t } = useI18n()
const { bem } = createNamespace('layout-mix-menu')
const { currentRoute } = useRouter()
const go = useGo()
let currentRouteRef = $ref<Nullable<RouteLocationNormalized>>(null)
let menuModulesRef = $ref<Menu[]>([])
let activePathRef = $ref<string>('')
let childrenMenus = $ref<Menu[]>([])
let openMenu = $ref(false)
let childrenTitle = $ref('')
const sideRef = ref<ElRef>(null)
const {
getIsFixed,
getIsMixSidebar,
getMixSideFixed,
getMixSideTrigger,
getCloseMixSidebarOnChange,
getMenuWidth,
getCollapsed,
getMenuType,
mixSideHasChildren,
setMenuSetting,
} = useMenuSetting()
const {
getChildrenMenus,
getCurrentParentPath,
getShallowMenus,
} = useMenu()
let oldIsFixed = unref(getIsFixed)
let pushpin = unref(getIsFixed)
onMounted(async () => {
menuModulesRef = await getShallowMenus()
activePathRef = await getCurrentParentPath(currentRoute.value.path)
const currentItem = menuModulesRef.find(item => item.path === activePathRef)
if (currentItem) {
handleModuleClick(currentItem.path, false, currentItem.meta?.title || '')
}
})
listenerRouteChange(async (route) => {
currentRouteRef = route
activePathRef = await getCurrentParentPath(route.path)
setActive(true)
if (unref(getCloseMixSidebarOnChange)) {
closeMenu()
}
})
// Set the currently active menu and submenu
async function setActive(setChildren = false) {
const path = currentRouteRef?.path
if (!path) {
return
}
// activePathRef = await getCurrentParentPath(path)
if (unref(getIsMixSidebar)) {
const activeMenu = unref(menuModulesRef).find(item => item.path === unref(activePathRef))
const p = activeMenu?.path
if (p) {
const children = await getChildrenMenus(p)
if (setChildren) {
childrenMenus = children
if (unref(getMixSideFixed)) {
openMenu = children.length > 0
}
}
if (children.length === 0) {
childrenMenus = []
}
}
}
mixSideHasChildren.value = childrenMenus.length > 0
}
function closeMenu() {
if (!unref(getIsFixed)) {
openMenu = false
setActive(false)
}
}
// Process module menu click
async function handleModuleClick(path: string, hover = false, title = '') {
const children = await getChildrenMenus(path)
childrenTitle = t(title)
if (unref(activePathRef) === path) {
if (!hover) {
if (!unref(openMenu)) {
openMenu = true
}
else {
closeMenu()
}
}
else {
if (!unref(openMenu)) {
openMenu = true
}
}
if (!unref(openMenu)) {
await setActive()
}
}
else {
openMenu = true
activePathRef = path
}
if (!children || children.length === 0) {
if (!hover) {
go(path)
}
childrenMenus = []
closeMenu()
return
}
childrenMenus = children
mixSideHasChildren.value = childrenMenus.length > 0
}
function getItemEvents(item: Menu) {
if (unref(getMixSideTrigger) === MixSidebarTriggerEnum.HOVER) {
return {
onMouseenter: () => {
return handleModuleClick(item.path, true, item.meta?.title || '')
},
onClick: async () => {
const children = await getChildrenMenus(item.path)
if (item.path && (!children || children.length === 0)) {
go(item.path)
}
},
}
}
return {
onClick: () => {
handleModuleClick(item.path, false, item.meta?.title || '')
},
}
}
const getMenuEventsRef = computed(() => {
return !unref(getMixSideFixed)
? {
onMouseleave: () => {
// 鼠标移出菜单不做操作
// if (!openMenu.value) {
// setActive(true)
// }
// 鼠标离开Menu 不触发关闭菜单面板
// closeMenu()
},
onMouseenter: () => {
},
}
: {}
})
function handleFixedMenu() {
setMenuSetting({
mixSideFixed: !unref(getMixSideFixed),
})
pushpin = !unref(getMixSideFixed)
}
const getMenuStyle = computed((): CSSProperties => {
if (unref(getIsFixed)) {
setActive(true)
}
else {
if (oldIsFixed !== unref(getIsFixed) && !pushpin) {
closeMenu()
}
else {
pushpin = false
}
}
oldIsFixed = unref(getIsFixed)
return {
width: unref(openMenu) ? `${unref(getMenuWidth)}px` : 0,
left: `${props.mixSidebarWidth}px`,
}
})
</script>
<template>
<div :class="bem()">
<Logo
v-if="getMenuType === NavBarModeEnum.MIX_SIDEBAR"
class="shadow"
:class="[bem('logo')]"
:style="{ '--un-shadow-color': 'var(--n-border-color)' }"
:show-title="false"
/>
<NScrollbar
:class="bem('scrollbar')"
v-bind="getMenuEventsRef"
>
<ul :class="bem('module')">
<li
v-for="item in menuModulesRef"
:key="item.path"
:class="[
bem('module__item'),
{
[bem('module__item--active') as string]: item.path === activePathRef,
},
]"
v-bind="getItemEvents(item)"
>
<LIcon
:class="bem('module__icon')"
:size="getCollapsed ? 16 : 20"
:icon="item.icon || (item.meta && item.meta.icon)"
/>
<p v-show="!getCollapsed" :class="bem('module__name')">
{{ $t(item.meta?.title || '') }}
</p>
</li>
</ul>
</NScrollbar>
<SiderTrigger />
<div
ref="sideRef"
class="shadow"
:class="[bem('menu-list')]"
:style="getMenuStyle"
@mouseleave="closeMenu"
>
<div
v-show="openMenu && childrenMenus.length > 0"
class="shadow"
:class="[
bem('menu-list__title'),
{
show: openMenu,
},
]"
>
<span class="text">{{ title }}</span>
<LIcon
:size="16"
:icon="getMixSideFixed ? 'ant-design:pushpin-filled' : 'ant-design:pushpin-outlined'"
class="pushpin hover:cursor-pointer"
@click="handleFixedMenu"
/>
</div>
<NH5
v-if="openMenu"
class="mb-2!"
:class="bem('menu-list__children-title')"
:style="{ '--n-text-color': 'var(--n-text-color)' }"
prefix="bar"
strong
>
{{ childrenTitle }}
</NH5>
<MixSubMenu :list="childrenMenus" />
</div>
</div>
</template>
<style lang="less" scoped>
.layout-mix-menu {
display: flex;
flex-direction: column;
height: 100%;
&__logo {
flex-shrink: 0;
justify-content: center;
}
&__scrollbar {
flex: 1;
flex-basis: auto;
}
&__module {
position: relative;
padding: 1px 0 40px 0;
margin: 0;
&__item {
position: relative;
padding: 12px 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: pointer;
transition: all 0.3s ease;
&:hover,
&--active {
// font-weight: 700;
color: #18a058;
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background-color: #18a058;
content: '';
}
}
}
&__icon {
transition: all 0.3s;
}
&__name {
margin-bottom: 0;
margin-top: 5px;
font-size: 12px;
transition: all 0.3s;
}
}
&__menu-list {
position: fixed;
height: 100%;
top: 0;
width: 0px;
height: calc(100%);
background-color: var(--n-color);
transition: all 0.3s;
&__title {
display: flex;
height: 48px;
font-size: 18px;
opacity: 0%;
transition: unset;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding-left: 20px;
&.show {
min-width: 130px;
opacity: 100%;
transition: all 0.5s ease;
}
.pushpin {
margin-right: 8px;
}
}
&__children-title {
padding: 6px 20px;
margin: 0;
}
}
}
</style>