Files
cn-rdms-web/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue

313 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<!-- deferBaseLayout 二次挂载时 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>