mirror of
https://github.com/halo-dev/halo.git
synced 2025-12-20 16:44:38 +08:00
feat: improve menu bar in mobile devices (#7542)
#### What type of PR is this? /area ui /kind improvement /milestone 2.21.x #### What this PR does / why we need it: <img width="408" alt="image" src="https://github.com/user-attachments/assets/296c631f-59e2-4aaa-9c0b-cabb0f8ee2d9" /> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/7410 Fixes https://github.com/halo-dev/halo/issues/2885 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 优化移动端的菜单,支持切换 Console / UC,支持退出登录和返回到首页。 ```
This commit is contained in:
@@ -2,55 +2,22 @@
|
||||
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import MobileMenu from "@/layouts/MobileMenu.vue";
|
||||
import UserProfileBanner from "@/layouts/UserProfileBanner.vue";
|
||||
import { isMac } from "@/utils/device";
|
||||
import { coreMenuGroups } from "@console/router/constant";
|
||||
import {
|
||||
Dialog,
|
||||
IconAccountCircleLine,
|
||||
IconArrowDownLine,
|
||||
IconLogoutCircleRLine,
|
||||
IconMore,
|
||||
IconSearch,
|
||||
IconShieldUser,
|
||||
VAvatar,
|
||||
VDropdown,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import { IconSearch } from "@halo-dev/components";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import {
|
||||
useOverlayScrollbars,
|
||||
type UseOverlayScrollbarsParams,
|
||||
} from "overlayscrollbars-vue";
|
||||
import { defineStore, storeToRefs } from "pinia";
|
||||
import { defineStore } from "pinia";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { RouterView, useRoute, useRouter } from "vue-router";
|
||||
import { RouterView, useRoute } from "vue-router";
|
||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const moreMenuVisible = ref(false);
|
||||
const moreMenuRootVisible = ref(false);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const { currentRoles, currentUser } = storeToRefs(userStore);
|
||||
|
||||
const handleLogout = () => {
|
||||
Dialog.warning({
|
||||
title: t("core.sidebar.operations.logout.title"),
|
||||
description: t("core.sidebar.operations.logout.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
window.location.href = "/logout";
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Global Search
|
||||
const globalSearchVisible = ref(false);
|
||||
@@ -103,223 +70,49 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-screen">
|
||||
<aside
|
||||
class="navbar fixed hidden h-full overflow-y-auto md:flex md:flex-col"
|
||||
>
|
||||
<div class="logo flex justify-center pb-5 pt-5">
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar__logo-container">
|
||||
<a
|
||||
href="/"
|
||||
target="_blank"
|
||||
:title="$t('core.sidebar.operations.visit_homepage.title')"
|
||||
>
|
||||
<IconLogo
|
||||
class="cursor-pointer select-none transition-all hover:brightness-125"
|
||||
/>
|
||||
<IconLogo class="sidebar__logo" />
|
||||
</a>
|
||||
</div>
|
||||
<div ref="navbarScroller" class="flex-1 overflow-y-hidden">
|
||||
<div class="px-3">
|
||||
<div
|
||||
class="flex cursor-pointer items-center rounded bg-gray-100 p-2 text-gray-400 transition-all hover:text-gray-900"
|
||||
@click="globalSearchVisible = true"
|
||||
>
|
||||
<span class="mr-3">
|
||||
<div ref="navbarScroller" class="sidebar__content">
|
||||
<div class="sidebar__search-wrapper">
|
||||
<div class="sidebar__search" @click="globalSearchVisible = true">
|
||||
<span class="sidebar__search-icon">
|
||||
<IconSearch />
|
||||
</span>
|
||||
<span class="flex-1 select-none text-base font-normal">
|
||||
<span class="sidebar__search-text">
|
||||
{{ $t("core.sidebar.search.placeholder") }}
|
||||
</span>
|
||||
<div class="text-sm">
|
||||
<div class="sidebar__search-shortcut">
|
||||
{{ `${isMac ? "⌘" : "Ctrl"}+K` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RoutesMenu :menus="menus" />
|
||||
</div>
|
||||
<div class="profile-placeholder">
|
||||
<div class="current-profile">
|
||||
<div v-if="currentUser?.spec.avatar" class="profile-avatar">
|
||||
<VAvatar
|
||||
:src="currentUser?.spec.avatar"
|
||||
:alt="currentUser?.spec.displayName"
|
||||
size="sm"
|
||||
circle
|
||||
></VAvatar>
|
||||
</div>
|
||||
<div class="profile-name">
|
||||
<div
|
||||
class="flex text-sm font-medium"
|
||||
:title="currentUser?.spec.displayName"
|
||||
>
|
||||
{{ currentUser?.spec.displayName }}
|
||||
</div>
|
||||
<div v-if="currentRoles?.length" class="flex mt-1">
|
||||
<VTag v-if="currentRoles.length === 1">
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
currentRoles[0].metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] || currentRoles[0].metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
<VDropdown v-else>
|
||||
<div class="flex gap-1">
|
||||
<VTag>
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||
</VTag>
|
||||
<IconArrowDownLine />
|
||||
</div>
|
||||
<template #popper>
|
||||
<div class="p-1">
|
||||
<h2
|
||||
class="text-gray-600 text-sm font-semibold border-b border-gray-100 pb-1.5"
|
||||
>
|
||||
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||
</h2>
|
||||
<div class="flex gap-2 flex-wrap mt-2">
|
||||
<VTag
|
||||
v-for="role in currentRoles"
|
||||
:key="role.metadata.name"
|
||||
>
|
||||
<template #leftIcon>
|
||||
<IconShieldUser />
|
||||
</template>
|
||||
{{
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISPLAY_NAME
|
||||
] || role.metadata.name
|
||||
}}
|
||||
</VTag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<a
|
||||
v-tooltip="$t('core.sidebar.operations.profile.tooltip')"
|
||||
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||
href="/uc"
|
||||
>
|
||||
<IconAccountCircleLine
|
||||
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
v-tooltip="$t('core.sidebar.operations.logout.tooltip')"
|
||||
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<IconLogoutCircleRLine
|
||||
class="h-5 w-5 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar__profile">
|
||||
<UserProfileBanner platform="console" />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="content w-full pb-12 mb-safe md:w-[calc(100%-16rem)] md:pb-0">
|
||||
<main class="main-content">
|
||||
<slot v-if="$slots.default" />
|
||||
<RouterView v-else />
|
||||
<footer
|
||||
v-if="!route.meta.hideFooter"
|
||||
class="mt-auto p-4 text-center text-sm"
|
||||
>
|
||||
<span class="text-gray-600">Powered by </span>
|
||||
<RouterLink to="/overview" class="hover:text-gray-600">
|
||||
<footer v-if="!route.meta.hideFooter" class="main-content__footer">
|
||||
<span class="main-content__footer-text">Powered by </span>
|
||||
<RouterLink to="/overview" class="main-content__footer-link">
|
||||
Halo
|
||||
</RouterLink>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!--bottom nav bar-->
|
||||
<div
|
||||
v-if="minimenus"
|
||||
class="bottom-nav-bar fixed bottom-0 left-0 right-0 grid grid-cols-6 border-t-2 border-black bg-secondary drop-shadow-2xl mt-safe pb-safe md:hidden"
|
||||
>
|
||||
<div
|
||||
v-for="(menu, index) in minimenus"
|
||||
:key="index"
|
||||
:class="{ 'bg-black': route.path === menu?.path }"
|
||||
class="nav-item"
|
||||
@click="router.push(menu?.path)"
|
||||
>
|
||||
<div
|
||||
class="flex w-full cursor-pointer items-center justify-center p-1 text-white"
|
||||
>
|
||||
<div class="flex h-10 w-10 flex-col items-center justify-center">
|
||||
<div class="text-base">
|
||||
<Component :is="menu?.icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" @click="moreMenuVisible = true">
|
||||
<div
|
||||
class="flex w-full cursor-pointer items-center justify-center p-1 text-white"
|
||||
>
|
||||
<div class="flex h-10 w-10 flex-col items-center justify-center">
|
||||
<div class="text-base">
|
||||
<IconMore />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-show="moreMenuRootVisible"
|
||||
class="drawer-wrapper fixed left-0 top-0 z-[99999] flex h-full w-full flex-row items-end justify-center"
|
||||
>
|
||||
<transition
|
||||
enter-active-class="ease-out duration-200"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="ease-in duration-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
@before-enter="moreMenuRootVisible = true"
|
||||
@after-leave="moreMenuRootVisible = false"
|
||||
>
|
||||
<div
|
||||
v-show="moreMenuVisible"
|
||||
class="drawer-layer absolute left-0 top-0 h-full w-full flex-none bg-gray-500 bg-opacity-75 transition-opacity"
|
||||
@click="moreMenuVisible = false"
|
||||
></div>
|
||||
</transition>
|
||||
<transition
|
||||
enter-active-class="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enter-from-class="translate-y-full"
|
||||
enter-to-class="translate-y-0"
|
||||
leave-active-class="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
leave-from-class="translate-y-0"
|
||||
leave-to-class="translate-y-full"
|
||||
>
|
||||
<div
|
||||
v-show="moreMenuVisible"
|
||||
class="drawer-content relative flex h-3/4 w-screen flex-col items-stretch overflow-y-auto rounded-t-md bg-white shadow-xl"
|
||||
>
|
||||
<div class="drawer-body">
|
||||
<RoutesMenu
|
||||
:menus="menus"
|
||||
class="p-0"
|
||||
@select="moreMenuVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
<MobileMenu :menus="menus" :minimenus="minimenus" platform="console" />
|
||||
</div>
|
||||
<GlobalSearchModal
|
||||
v-if="globalSearchVisible"
|
||||
@@ -328,47 +121,117 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar {
|
||||
@apply w-64;
|
||||
@apply bg-white;
|
||||
@apply shadow;
|
||||
.layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
width: theme("width.64");
|
||||
height: 100%;
|
||||
background-color: theme("colors.white");
|
||||
box-shadow: theme("boxShadow.DEFAULT");
|
||||
z-index: 999;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
|
||||
.profile-placeholder {
|
||||
height: 70px;
|
||||
flex: none;
|
||||
@media (min-width: theme("screens.md")) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.current-profile {
|
||||
height: 70px;
|
||||
@apply fixed
|
||||
bottom-0
|
||||
left-0
|
||||
flex
|
||||
w-64
|
||||
gap-3
|
||||
bg-white
|
||||
p-3;
|
||||
&__logo-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
@apply flex
|
||||
items-center
|
||||
self-center;
|
||||
}
|
||||
&__logo {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all;
|
||||
|
||||
.profile-name {
|
||||
@apply flex-1
|
||||
self-center
|
||||
overflow-hidden;
|
||||
}
|
||||
&:hover {
|
||||
filter: brightness(1.25);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
&__search-wrapper {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
&__search {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
background-color: theme("colors.gray.100");
|
||||
padding: 0.5rem;
|
||||
color: theme("colors.gray.400");
|
||||
transition: all;
|
||||
|
||||
&:hover {
|
||||
color: theme("colors.gray.900");
|
||||
}
|
||||
}
|
||||
|
||||
&__search-icon {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
&__search-text {
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&__search-shortcut {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&__profile {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply ml-0
|
||||
flex
|
||||
flex-auto
|
||||
flex-col
|
||||
md:ml-64;
|
||||
.main-content {
|
||||
width: 100%;
|
||||
padding-bottom: 3rem;
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
display: flex;
|
||||
flex: auto;
|
||||
flex-direction: column;
|
||||
|
||||
@media (min-width: theme("screens.md")) {
|
||||
width: calc(100% - 16rem);
|
||||
margin-left: theme("width.64");
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: auto;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&__footer-text {
|
||||
color: theme("colors.gray.600");
|
||||
}
|
||||
|
||||
&__footer-link {
|
||||
&:hover {
|
||||
color: theme("colors.gray.600");
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user