313 lines
8.6 KiB
Vue
313 lines
8.6 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from 'vue';
|
||
import { useRoute } from 'vue-router';
|
||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
|
||
import { getObjectContextDomainConfigByPath } from '@/constants/object-context';
|
||
import { useAppStore } from '@/store/modules/app';
|
||
import { useObjectContextStore } from '@/store/modules/object-context';
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import { useRouterPush } from '@/hooks/common/router';
|
||
import FirstLevelMenu from '../components/first-level-menu.vue';
|
||
import ObjectContextSwitcher from '../components/object-context-switcher.vue';
|
||
import { useMenu, useMixMenuContext } from '../../../context';
|
||
|
||
defineOptions({
|
||
name: 'HorizontalMixMenu'
|
||
});
|
||
|
||
const appStore = useAppStore();
|
||
const route = useRoute();
|
||
const objectContextStore = useObjectContextStore();
|
||
const themeStore = useThemeStore();
|
||
const { routerPush, routerPushByKeyWithMetaQuery } = useRouterPush();
|
||
const {
|
||
allMenus,
|
||
headerMenuMode,
|
||
headerMenus,
|
||
currentObjectContextDomain,
|
||
activeFirstLevelMenuKey,
|
||
setActiveFirstLevelMenuKey
|
||
} = useMixMenuContext();
|
||
const { selectedKey } = useMenu();
|
||
|
||
const activeFirstLevelMenu = computed(
|
||
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value) || null
|
||
);
|
||
const headerMenuHeight = computed(() => `${themeStore.header.height}px`);
|
||
const showObjectContextInfo = computed(
|
||
() => headerMenuMode.value === 'object-context' && objectContextStore.hasContext
|
||
);
|
||
const activeHeaderMenuKey = computed(() =>
|
||
headerMenuMode.value === 'object-context' ? String(route.name || '') : selectedKey.value
|
||
);
|
||
|
||
function handleSelectMixMenu(menu: App.Global.Menu) {
|
||
setActiveFirstLevelMenuKey(menu.key);
|
||
|
||
const domainConfig = getObjectContextDomainConfigByPath(menu.routePath);
|
||
|
||
if (domainConfig) {
|
||
objectContextStore.clearContext();
|
||
routerPush({ path: domainConfig.entryRoutePath });
|
||
return;
|
||
}
|
||
|
||
routerPushByKeyWithMetaQuery(menu.routeKey);
|
||
}
|
||
|
||
function handleClickNavItem(menu: App.Global.Menu | App.ObjectContext.Menu) {
|
||
if (headerMenuMode.value === 'object-context') {
|
||
const location = objectContextStore.getMenuRouteLocation(menu as App.ObjectContext.Menu);
|
||
|
||
if (location) {
|
||
routerPush(location);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
routerPushByKeyWithMetaQuery((menu as App.Global.Menu).routeKey);
|
||
}
|
||
|
||
function handleClickDomainAnchor() {
|
||
if (currentObjectContextDomain.value) {
|
||
objectContextStore.clearContext();
|
||
routerPush({ path: currentObjectContextDomain.value.entryRoutePath });
|
||
return;
|
||
}
|
||
|
||
if (!activeFirstLevelMenu.value) {
|
||
return;
|
||
}
|
||
|
||
routerPushByKeyWithMetaQuery(activeFirstLevelMenu.value.routeKey);
|
||
}
|
||
|
||
function isMenuActive(menu: App.Global.Menu | App.ObjectContext.Menu): boolean {
|
||
if (menu.key === activeHeaderMenuKey.value) {
|
||
return true;
|
||
}
|
||
|
||
return menu.children?.some(child => isMenuActive(child)) || false;
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- defer:BaseLayout 二次挂载时 GlobalMenu 已缓存为同步挂载,目标 div 还未插入 document,不延迟解析会静默失败且不重试 -->
|
||
<Teleport defer :to="`#${GLOBAL_HEADER_MENU_ID}`">
|
||
<div class="mix-header-nav size-full min-w-0 flex-y-center">
|
||
<button
|
||
v-if="activeFirstLevelMenu"
|
||
type="button"
|
||
class="domain-anchor h-full flex-y-center gap-8px px-8px text-left"
|
||
@click="handleClickDomainAnchor"
|
||
>
|
||
<component :is="activeFirstLevelMenu.icon" v-if="activeFirstLevelMenu.icon" class="text-icon" />
|
||
<span class="domain-anchor__label">{{ activeFirstLevelMenu.label }}</span>
|
||
</button>
|
||
<div
|
||
v-if="showObjectContextInfo || headerMenus.length"
|
||
class="mx-12px h-20px w-1px shrink-0 bg-[var(--el-border-color)]"
|
||
></div>
|
||
<div v-if="showObjectContextInfo" class="context-object-tag h-full flex-y-center">
|
||
<ObjectContextSwitcher v-if="currentObjectContextDomain" :domain-config="currentObjectContextDomain" />
|
||
</div>
|
||
<div
|
||
v-if="showObjectContextInfo && headerMenus.length"
|
||
class="mx-12px h-20px w-1px shrink-0 bg-[var(--el-border-color)]"
|
||
></div>
|
||
<div v-if="headerMenus.length" class="header-nav-list h-full min-w-0 flex-1">
|
||
<template v-for="item in headerMenus" :key="item.key">
|
||
<button
|
||
v-if="!item.children?.length"
|
||
type="button"
|
||
class="header-nav-item"
|
||
:class="{ 'is-active': isMenuActive(item) }"
|
||
@click="handleClickNavItem(item)"
|
||
>
|
||
<span class="header-nav-item__label">{{ item.label }}</span>
|
||
</button>
|
||
<ElDropdown
|
||
v-else
|
||
trigger="hover"
|
||
placement="bottom"
|
||
popper-class="header-nav-dropdown"
|
||
:show-timeout="120"
|
||
:hide-timeout="120"
|
||
:teleported="true"
|
||
>
|
||
<button
|
||
type="button"
|
||
class="header-nav-item header-nav-item--dropdown"
|
||
:class="{ 'is-active': isMenuActive(item) }"
|
||
>
|
||
<span class="header-nav-item__label">{{ item.label }}</span>
|
||
<icon-ep:arrow-down class="header-nav-item__arrow" />
|
||
</button>
|
||
<template #dropdown>
|
||
<ElDropdownMenu>
|
||
<ElDropdownItem
|
||
v-for="child in item.children"
|
||
:key="child.key"
|
||
class="header-nav-dropdown__item"
|
||
:class="{ 'is-active-route': isMenuActive(child) }"
|
||
@click="handleClickNavItem(child)"
|
||
>
|
||
{{ child.label }}
|
||
</ElDropdownItem>
|
||
</ElDropdownMenu>
|
||
</template>
|
||
</ElDropdown>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
<Teleport defer :to="`#${GLOBAL_SIDER_MENU_ID}`">
|
||
<FirstLevelMenu
|
||
:menus="allMenus"
|
||
:active-menu-key="activeFirstLevelMenuKey"
|
||
:sider-collapse="appStore.siderCollapse"
|
||
:dark-mode="themeStore.darkMode"
|
||
:theme-color="themeStore.themeColor"
|
||
@select="handleSelectMixMenu"
|
||
@toggle-sider-collapse="appStore.toggleSiderCollapse"
|
||
/>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.mix-header-nav {
|
||
height: v-bind(headerMenuHeight);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.domain-anchor {
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
border: none;
|
||
background: transparent;
|
||
margin: 0;
|
||
padding-top: 0;
|
||
padding-bottom: 0;
|
||
font: inherit;
|
||
flex-shrink: 0;
|
||
min-width: 0;
|
||
line-height: 1;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.domain-anchor:hover {
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.domain-anchor__label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
max-width: 12rem;
|
||
line-height: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.header-nav-list {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header-nav-item {
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
position: relative;
|
||
margin: 0;
|
||
height: 100%;
|
||
flex-shrink: 0;
|
||
padding: 0 14px;
|
||
border: none;
|
||
background: transparent;
|
||
font: inherit;
|
||
line-height: 1;
|
||
color: var(--el-text-color-primary);
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.header-nav-item:hover {
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.header-nav-item__label {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
line-height: 1;
|
||
}
|
||
|
||
.header-nav-item__arrow {
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.header-nav-item.is-active {
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.header-nav-item.is-active::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: 0;
|
||
height: 2px;
|
||
border-radius: 999px;
|
||
background-color: var(--el-color-primary);
|
||
}
|
||
|
||
:global(.header-nav-dropdown.el-popper) {
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 14px;
|
||
background-color: rgb(255 255 255 / 98%);
|
||
box-shadow:
|
||
0 12px 28px rgb(15 23 42 / 10%),
|
||
0 2px 8px rgb(15 23 42 / 6%);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
:global(.header-nav-dropdown .el-popper__arrow) {
|
||
display: none;
|
||
}
|
||
|
||
:global(.header-nav-dropdown .el-dropdown-menu) {
|
||
padding: 8px;
|
||
border: 1px solid rgb(226 232 240 / 90%);
|
||
border-radius: 14px;
|
||
box-shadow: none;
|
||
}
|
||
|
||
:global(.header-nav-dropdown .el-dropdown-menu__item) {
|
||
height: 40px;
|
||
margin: 2px 0;
|
||
padding: 0 12px;
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
line-height: 40px;
|
||
color: rgb(15 23 42 / 88%);
|
||
}
|
||
|
||
:global(.header-nav-dropdown .el-dropdown-menu__item:hover) {
|
||
background-color: rgb(99 102 241 / 8%);
|
||
}
|
||
|
||
:global(.header-nav-dropdown .el-dropdown-menu__item.is-active-route) {
|
||
color: var(--el-color-primary);
|
||
background-color: rgb(99 102 241 / 10%);
|
||
}
|
||
</style>
|