mirror of https://github.com/halo-dev/halo
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,支持退出登录和返回到首页。 ```pull/7549/head
parent
4a3d35a900
commit
663ee22147
|
|
@ -2,55 +2,22 @@
|
||||||
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
||||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import MobileMenu from "@/layouts/MobileMenu.vue";
|
||||||
import { useUserStore } from "@/stores/user";
|
import UserProfileBanner from "@/layouts/UserProfileBanner.vue";
|
||||||
import { isMac } from "@/utils/device";
|
import { isMac } from "@/utils/device";
|
||||||
import { coreMenuGroups } from "@console/router/constant";
|
import { coreMenuGroups } from "@console/router/constant";
|
||||||
import {
|
import { IconSearch } from "@halo-dev/components";
|
||||||
Dialog,
|
|
||||||
IconAccountCircleLine,
|
|
||||||
IconArrowDownLine,
|
|
||||||
IconLogoutCircleRLine,
|
|
||||||
IconMore,
|
|
||||||
IconSearch,
|
|
||||||
IconShieldUser,
|
|
||||||
VAvatar,
|
|
||||||
VDropdown,
|
|
||||||
VTag,
|
|
||||||
} from "@halo-dev/components";
|
|
||||||
import { useEventListener } from "@vueuse/core";
|
import { useEventListener } from "@vueuse/core";
|
||||||
import {
|
import {
|
||||||
useOverlayScrollbars,
|
useOverlayScrollbars,
|
||||||
type UseOverlayScrollbarsParams,
|
type UseOverlayScrollbarsParams,
|
||||||
} from "overlayscrollbars-vue";
|
} from "overlayscrollbars-vue";
|
||||||
import { defineStore, storeToRefs } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { RouterView, useRoute } from "vue-router";
|
||||||
import { RouterView, useRoute, useRouter } from "vue-router";
|
|
||||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||||
|
|
||||||
const route = useRoute();
|
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
|
// Global Search
|
||||||
const globalSearchVisible = ref(false);
|
const globalSearchVisible = ref(false);
|
||||||
|
|
@ -103,223 +70,49 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-screen">
|
<div class="layout">
|
||||||
<aside
|
<aside class="sidebar">
|
||||||
class="navbar fixed hidden h-full overflow-y-auto md:flex md:flex-col"
|
<div class="sidebar__logo-container">
|
||||||
>
|
|
||||||
<div class="logo flex justify-center pb-5 pt-5">
|
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="$t('core.sidebar.operations.visit_homepage.title')"
|
:title="$t('core.sidebar.operations.visit_homepage.title')"
|
||||||
>
|
>
|
||||||
<IconLogo
|
<IconLogo class="sidebar__logo" />
|
||||||
class="cursor-pointer select-none transition-all hover:brightness-125"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div ref="navbarScroller" class="flex-1 overflow-y-hidden">
|
<div ref="navbarScroller" class="sidebar__content">
|
||||||
<div class="px-3">
|
<div class="sidebar__search-wrapper">
|
||||||
<div
|
<div class="sidebar__search" @click="globalSearchVisible = true">
|
||||||
class="flex cursor-pointer items-center rounded bg-gray-100 p-2 text-gray-400 transition-all hover:text-gray-900"
|
<span class="sidebar__search-icon">
|
||||||
@click="globalSearchVisible = true"
|
|
||||||
>
|
|
||||||
<span class="mr-3">
|
|
||||||
<IconSearch />
|
<IconSearch />
|
||||||
</span>
|
</span>
|
||||||
<span class="flex-1 select-none text-base font-normal">
|
<span class="sidebar__search-text">
|
||||||
{{ $t("core.sidebar.search.placeholder") }}
|
{{ $t("core.sidebar.search.placeholder") }}
|
||||||
</span>
|
</span>
|
||||||
<div class="text-sm">
|
<div class="sidebar__search-shortcut">
|
||||||
{{ `${isMac ? "⌘" : "Ctrl"}+K` }}
|
{{ `${isMac ? "⌘" : "Ctrl"}+K` }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RoutesMenu :menus="menus" />
|
<RoutesMenu :menus="menus" />
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-placeholder">
|
<div class="sidebar__profile">
|
||||||
<div class="current-profile">
|
<UserProfileBanner platform="console" />
|
||||||
<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>
|
</div>
|
||||||
</aside>
|
</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" />
|
<slot v-if="$slots.default" />
|
||||||
<RouterView v-else />
|
<RouterView v-else />
|
||||||
<footer
|
<footer v-if="!route.meta.hideFooter" class="main-content__footer">
|
||||||
v-if="!route.meta.hideFooter"
|
<span class="main-content__footer-text">Powered by </span>
|
||||||
class="mt-auto p-4 text-center text-sm"
|
<RouterLink to="/overview" class="main-content__footer-link">
|
||||||
>
|
|
||||||
<span class="text-gray-600">Powered by </span>
|
|
||||||
<RouterLink to="/overview" class="hover:text-gray-600">
|
|
||||||
Halo
|
Halo
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
<MobileMenu :menus="menus" :minimenus="minimenus" platform="console" />
|
||||||
<!--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>
|
|
||||||
</div>
|
</div>
|
||||||
<GlobalSearchModal
|
<GlobalSearchModal
|
||||||
v-if="globalSearchVisible"
|
v-if="globalSearchVisible"
|
||||||
|
|
@ -328,47 +121,117 @@ onMounted(() => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.navbar {
|
.layout {
|
||||||
@apply w-64;
|
display: flex;
|
||||||
@apply bg-white;
|
min-height: 100vh;
|
||||||
@apply shadow;
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
width: theme("width.64");
|
||||||
|
height: 100%;
|
||||||
|
background-color: theme("colors.white");
|
||||||
|
box-shadow: theme("boxShadow.DEFAULT");
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.profile-placeholder {
|
@media (min-width: theme("screens.md")) {
|
||||||
height: 70px;
|
display: flex;
|
||||||
flex: none;
|
}
|
||||||
|
|
||||||
.current-profile {
|
&__logo-container {
|
||||||
height: 70px;
|
display: flex;
|
||||||
@apply fixed
|
justify-content: center;
|
||||||
bottom-0
|
padding-top: 1.25rem;
|
||||||
left-0
|
padding-bottom: 1.25rem;
|
||||||
flex
|
}
|
||||||
w-64
|
|
||||||
gap-3
|
|
||||||
bg-white
|
|
||||||
p-3;
|
|
||||||
|
|
||||||
.profile-avatar {
|
&__logo {
|
||||||
@apply flex
|
cursor: pointer;
|
||||||
items-center
|
user-select: none;
|
||||||
self-center;
|
transition: all;
|
||||||
}
|
|
||||||
|
|
||||||
.profile-name {
|
&:hover {
|
||||||
@apply flex-1
|
filter: brightness(1.25);
|
||||||
self-center
|
|
||||||
overflow-hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__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 {
|
.main-content {
|
||||||
@apply ml-0
|
width: 100%;
|
||||||
flex
|
padding-bottom: 3rem;
|
||||||
flex-auto
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
flex-col
|
display: flex;
|
||||||
md:ml-64;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import { i18n } from "@/locales";
|
||||||
import { useGlobalInfoStore } from "@/stores/global-info";
|
import { useGlobalInfoStore } from "@/stores/global-info";
|
||||||
import type { FormKitConfig } from "@formkit/core";
|
import type { FormKitConfig } from "@formkit/core";
|
||||||
import { useFavicon } from "@vueuse/core";
|
import { useFavicon } from "@vueuse/core";
|
||||||
|
import type { OverlayScrollbars } from "overlayscrollbars";
|
||||||
import {
|
import {
|
||||||
useOverlayScrollbars,
|
useOverlayScrollbars,
|
||||||
type UseOverlayScrollbarsParams,
|
type UseOverlayScrollbarsParams,
|
||||||
} from "overlayscrollbars-vue";
|
} from "overlayscrollbars-vue";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { computed, inject, onMounted, reactive } from "vue";
|
import { computed, inject, onMounted, provide, reactive } from "vue";
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
|
|
||||||
useAppTitle();
|
useAppTitle();
|
||||||
|
|
@ -35,11 +36,13 @@ const reactiveParams = reactive<UseOverlayScrollbarsParams>({
|
||||||
},
|
},
|
||||||
defer: true,
|
defer: true,
|
||||||
});
|
});
|
||||||
const [initialize] = useOverlayScrollbars(reactiveParams);
|
const [initialize, instance] = useOverlayScrollbars(reactiveParams);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (body) initialize({ target: body });
|
if (body) initialize({ target: body });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provide<() => OverlayScrollbars | null>("bodyScrollInstance", instance);
|
||||||
|
|
||||||
// setup formkit locale
|
// setup formkit locale
|
||||||
// see https://formkit.com/essentials/internationalization
|
// see https://formkit.com/essentials/internationalization
|
||||||
const formkitLocales = {
|
const formkitLocales = {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
|
import { IconMore, VMenu, VMenuItem } from "@halo-dev/components";
|
||||||
|
import type { OverlayScrollbars } from "overlayscrollbars";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
|
import type { MenuGroupType, MenuItemType } from "packages/shared/dist";
|
||||||
|
import { inject, ref, watch } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import RiArrowLeftLine from "~icons/ri/arrow-left-line";
|
||||||
|
import UserProfileBanner from "./UserProfileBanner.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
platform?: "console" | "uc";
|
||||||
|
menus: MenuGroupType[];
|
||||||
|
minimenus: MenuItemType[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const moreMenuVisible = ref(false);
|
||||||
|
|
||||||
|
const bodyScrollInstance =
|
||||||
|
inject<() => OverlayScrollbars | null>("bodyScrollInstance");
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => moreMenuVisible.value,
|
||||||
|
(value) => {
|
||||||
|
// Lock body scroll when the drawer is open
|
||||||
|
bodyScrollInstance?.()?.options({
|
||||||
|
overflow: {
|
||||||
|
x: value ? "hidden" : "scroll",
|
||||||
|
y: value ? "hidden" : "scroll",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSelectHome() {
|
||||||
|
window.open("/", "_blank");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div v-if="minimenus" class="mobile-nav mobile-nav--fixed">
|
||||||
|
<div
|
||||||
|
v-for="(menu, index) in minimenus"
|
||||||
|
:key="index"
|
||||||
|
:class="{ 'mobile-nav__item--active': route.path === menu?.path }"
|
||||||
|
class="mobile-nav__item"
|
||||||
|
@click="$router.push(menu?.path)"
|
||||||
|
>
|
||||||
|
<div class="mobile-nav__button">
|
||||||
|
<div class="mobile-nav__icon-container">
|
||||||
|
<div class="mobile-nav__icon">
|
||||||
|
<Component :is="menu?.icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mobile-nav__item" @click="moreMenuVisible = true">
|
||||||
|
<div class="mobile-nav__button">
|
||||||
|
<div class="mobile-nav__icon-container">
|
||||||
|
<div class="mobile-nav__icon">
|
||||||
|
<IconMore />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="moreMenuVisible" class="drawer drawer--visible">
|
||||||
|
<transition
|
||||||
|
enter-active-class="ease-out duration-400"
|
||||||
|
enter-from-class="opacity-0"
|
||||||
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="ease-in duration-200"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
appear
|
||||||
|
>
|
||||||
|
<div class="drawer__overlay" @click="moreMenuVisible = false"></div>
|
||||||
|
</transition>
|
||||||
|
<transition
|
||||||
|
enter-active-class="transform transition ease-out duration-500"
|
||||||
|
enter-from-class="translate-y-full scale-95"
|
||||||
|
enter-to-class="translate-y-0 scale-100"
|
||||||
|
leave-active-class="transform transition ease-in duration-300"
|
||||||
|
leave-from-class="translate-y-0 scale-100"
|
||||||
|
leave-to-class="translate-y-full scale-95"
|
||||||
|
appear
|
||||||
|
>
|
||||||
|
<div class="drawer__content">
|
||||||
|
<OverlayScrollbarsComponent
|
||||||
|
element="div"
|
||||||
|
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||||
|
class="drawer__body"
|
||||||
|
defer
|
||||||
|
>
|
||||||
|
<VMenu class="!pb-1">
|
||||||
|
<VMenuItem
|
||||||
|
id="home"
|
||||||
|
:title="$t('core.sidebar.menu.items.home')"
|
||||||
|
@select="handleSelectHome"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<RiArrowLeftLine />
|
||||||
|
</template>
|
||||||
|
</VMenuItem>
|
||||||
|
</VMenu>
|
||||||
|
<RoutesMenu :menus="menus" @select="moreMenuVisible = false" />
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
<div class="drawer__footer">
|
||||||
|
<UserProfileBanner :platform="platform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.mobile-nav {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
border-top: 2px solid theme("colors.black");
|
||||||
|
background-color: theme("colors.secondary");
|
||||||
|
box-shadow: theme("dropShadow.2xl");
|
||||||
|
margin-top: env(safe-area-inset-top);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
|
||||||
|
@media (min-width: theme("screens.md")) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
&--active {
|
||||||
|
background-color: theme("colors.black");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
color: theme("colors.white");
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon-container {
|
||||||
|
display: flex;
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
flex: none;
|
||||||
|
background-color: theme("colors.gray.500");
|
||||||
|
opacity: 0.75;
|
||||||
|
transition: opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
height: 75%;
|
||||||
|
width: 100vw;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
border-top-left-radius: 0.75rem;
|
||||||
|
border-top-right-radius: 0.75rem;
|
||||||
|
background-color: theme("colors.white");
|
||||||
|
box-shadow: theme("boxShadow.xl");
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
width: 100%;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
IconAccountCircleLine,
|
||||||
|
IconArrowDownLine,
|
||||||
|
IconLogoutCircleRLine,
|
||||||
|
IconSettings3Line,
|
||||||
|
IconShieldUser,
|
||||||
|
VAvatar,
|
||||||
|
VDropdown,
|
||||||
|
VTag,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
platform?: "console" | "uc";
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
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";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const disallowAccessConsole = computed(() => {
|
||||||
|
if (
|
||||||
|
currentRoles?.value?.some((role) => role.metadata.name === SUPER_ROLE_NAME)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDisallowAccessConsoleRole = currentRoles?.value?.some((role) => {
|
||||||
|
return (
|
||||||
|
role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return !!hasDisallowAccessConsoleRole;
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = computed(() => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: t("core.sidebar.operations.logout.tooltip"),
|
||||||
|
icon: IconLogoutCircleRLine,
|
||||||
|
onClick: handleLogout,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (props.platform === "console") {
|
||||||
|
items.unshift({
|
||||||
|
label: t("core.sidebar.operations.profile.tooltip"),
|
||||||
|
icon: IconAccountCircleLine,
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = "/uc";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.platform === "uc" && !disallowAccessConsole.value) {
|
||||||
|
items.unshift({
|
||||||
|
label: t("core.uc_sidebar.operations.console.tooltip"),
|
||||||
|
icon: IconSettings3Line,
|
||||||
|
onClick: () => {
|
||||||
|
window.location.href = "/console";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="user-profile">
|
||||||
|
<div v-if="currentUser?.spec.avatar" class="user-profile__avatar">
|
||||||
|
<VAvatar
|
||||||
|
:src="currentUser?.spec.avatar"
|
||||||
|
:alt="currentUser?.spec.displayName"
|
||||||
|
size="sm"
|
||||||
|
circle
|
||||||
|
></VAvatar>
|
||||||
|
</div>
|
||||||
|
<div class="user-profile__info">
|
||||||
|
<div class="user-profile__name" :title="currentUser?.spec.displayName">
|
||||||
|
{{ currentUser?.spec.displayName }}
|
||||||
|
</div>
|
||||||
|
<div v-if="currentRoles?.length" class="user-profile__roles">
|
||||||
|
<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 :triggers="['click']">
|
||||||
|
<div class="user-profile__roles-dropdown">
|
||||||
|
<VTag>
|
||||||
|
<template #leftIcon>
|
||||||
|
<IconShieldUser />
|
||||||
|
</template>
|
||||||
|
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||||
|
</VTag>
|
||||||
|
<IconArrowDownLine />
|
||||||
|
</div>
|
||||||
|
<template #popper>
|
||||||
|
<div class="user-profile__roles-popper">
|
||||||
|
<h2 class="user-profile__roles-title">
|
||||||
|
{{ $t("core.sidebar.profile.aggregate_role") }}
|
||||||
|
</h2>
|
||||||
|
<div class="user-profile__roles-list">
|
||||||
|
<VTag
|
||||||
|
v-for="role in currentRoles"
|
||||||
|
:key="role.metadata.name"
|
||||||
|
class="user-profile__role-tag"
|
||||||
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<IconShieldUser />
|
||||||
|
</template>
|
||||||
|
{{
|
||||||
|
role.metadata.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
|
||||||
|
role.metadata.name
|
||||||
|
}}
|
||||||
|
</VTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-profile__actions">
|
||||||
|
<button
|
||||||
|
v-for="action in actions"
|
||||||
|
:key="action.label"
|
||||||
|
v-tooltip="action.label"
|
||||||
|
class="user-profile__action-button"
|
||||||
|
@click="action.onClick"
|
||||||
|
>
|
||||||
|
<component :is="action.icon" class="user-profile__action-icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user-profile {
|
||||||
|
height: 70px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.75rem;
|
||||||
|
background-color: theme("colors.white");
|
||||||
|
padding: 0.75rem;
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1;
|
||||||
|
align-self: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: flex;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__roles {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__roles-dropdown {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__roles-popper {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__roles-title {
|
||||||
|
color: theme("colors.gray.600");
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px solid theme("colors.gray.100");
|
||||||
|
padding-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__roles-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action-button {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0.375rem;
|
||||||
|
transition: all;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: theme("colors.gray.100");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action-icon {
|
||||||
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
color: theme("colors.gray.600");
|
||||||
|
|
||||||
|
.user-profile__action-button:hover & {
|
||||||
|
color: theme("colors.gray.900");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,6 +2,7 @@ core:
|
||||||
sidebar:
|
sidebar:
|
||||||
menu:
|
menu:
|
||||||
items:
|
items:
|
||||||
|
home: Home
|
||||||
tools: Tools
|
tools: Tools
|
||||||
operations:
|
operations:
|
||||||
logout:
|
logout:
|
||||||
|
|
@ -30,7 +31,6 @@ core:
|
||||||
label: Enable animation
|
label: Enable animation
|
||||||
presets:
|
presets:
|
||||||
recent_published:
|
recent_published:
|
||||||
publishTime: Publish Time {publishTime}
|
|
||||||
empty:
|
empty:
|
||||||
title: No published posts
|
title: No published posts
|
||||||
notification:
|
notification:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ core:
|
||||||
system: System
|
system: System
|
||||||
tool: Tool
|
tool: Tool
|
||||||
items:
|
items:
|
||||||
|
home: Home
|
||||||
dashboard: Dashboard
|
dashboard: Dashboard
|
||||||
posts: Posts
|
posts: Posts
|
||||||
single_pages: Pages
|
single_pages: Pages
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ core:
|
||||||
overview: 概览
|
overview: 概览
|
||||||
backup: 备份
|
backup: 备份
|
||||||
tools: 工具
|
tools: 工具
|
||||||
|
home: 首页
|
||||||
operations:
|
operations:
|
||||||
logout:
|
logout:
|
||||||
tooltip: 退出登录
|
tooltip: 退出登录
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ core:
|
||||||
overview: 概覽
|
overview: 概覽
|
||||||
backup: 備份
|
backup: 備份
|
||||||
tools: 工具
|
tools: 工具
|
||||||
|
home: 首页
|
||||||
operations:
|
operations:
|
||||||
logout:
|
logout:
|
||||||
tooltip: 登出
|
tooltip: 登出
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,19 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import MobileMenu from "@/layouts/MobileMenu.vue";
|
||||||
import { SUPER_ROLE_NAME } from "@/constants/constants";
|
import UserProfileBanner from "@/layouts/UserProfileBanner.vue";
|
||||||
import { useUserStore } from "@/stores/user";
|
|
||||||
import { coreMenuGroups } from "@console/router/constant";
|
import { coreMenuGroups } from "@console/router/constant";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
IconArrowDownLine,
|
|
||||||
IconLogoutCircleRLine,
|
|
||||||
IconMore,
|
|
||||||
IconSettings3Line,
|
|
||||||
IconShieldUser,
|
|
||||||
VAvatar,
|
|
||||||
VDropdown,
|
|
||||||
VTag,
|
|
||||||
} from "@halo-dev/components";
|
|
||||||
import {
|
import {
|
||||||
useOverlayScrollbars,
|
useOverlayScrollbars,
|
||||||
type UseOverlayScrollbarsParams,
|
type UseOverlayScrollbarsParams,
|
||||||
} from "overlayscrollbars-vue";
|
} from "overlayscrollbars-vue";
|
||||||
import { defineStore, storeToRefs } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { RouterView, useRoute } from "vue-router";
|
||||||
import { RouterView, useRoute, useRouter } from "vue-router";
|
|
||||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||||
|
|
||||||
const route = useRoute();
|
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";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { menus, minimenus } = useRouteMenuGenerator(coreMenuGroups);
|
const { menus, minimenus } = useRouteMenuGenerator(coreMenuGroups);
|
||||||
|
|
||||||
|
|
@ -87,272 +53,123 @@ onMounted(() => {
|
||||||
initialize({ target: navbarScroller.value });
|
initialize({ target: navbarScroller.value });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const disallowAccessConsole = computed(() => {
|
|
||||||
if (
|
|
||||||
currentRoles?.value?.some((role) => role.metadata.name === SUPER_ROLE_NAME)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasDisallowAccessConsoleRole = currentRoles?.value?.some((role) => {
|
|
||||||
return (
|
|
||||||
role.metadata.annotations?.[rbacAnnotations.DISALLOW_ACCESS_CONSOLE] ===
|
|
||||||
"true"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return !!hasDisallowAccessConsoleRole;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-screen">
|
<div class="layout">
|
||||||
<aside
|
<aside class="sidebar">
|
||||||
class="navbar fixed hidden h-full overflow-y-auto md:flex md:flex-col"
|
<div class="sidebar__logo-container">
|
||||||
>
|
|
||||||
<div class="logo flex justify-center pb-5 pt-5">
|
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:title="$t('core.sidebar.operations.visit_homepage.title')"
|
:title="$t('core.sidebar.operations.visit_homepage.title')"
|
||||||
>
|
>
|
||||||
<IconLogo
|
<IconLogo class="sidebar__logo" />
|
||||||
class="cursor-pointer select-none transition-all hover:brightness-125"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div ref="navbarScroller" class="flex-1 overflow-y-hidden">
|
<div ref="navbarScroller" class="sidebar__content">
|
||||||
<RoutesMenu :menus="menus" />
|
<RoutesMenu :menus="menus" />
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-placeholder">
|
<div class="sidebar__profile">
|
||||||
<div class="current-profile">
|
<UserProfileBanner platform="uc" />
|
||||||
<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">
|
|
||||||
{{ 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.uc_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.uc_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-if="!disallowAccessConsole"
|
|
||||||
v-tooltip="$t('core.uc_sidebar.operations.console.tooltip')"
|
|
||||||
class="group inline-block cursor-pointer rounded-full p-1.5 transition-all hover:bg-gray-100"
|
|
||||||
href="/console"
|
|
||||||
>
|
|
||||||
<IconSettings3Line
|
|
||||||
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>
|
</div>
|
||||||
</aside>
|
</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" />
|
<slot v-if="$slots.default" />
|
||||||
<RouterView v-else />
|
<RouterView v-else />
|
||||||
<footer
|
<footer v-if="!route.meta.hideFooter" class="main-content__footer">
|
||||||
v-if="!route.meta.hideFooter"
|
<span class="main-content__footer-text">Powered by </span>
|
||||||
class="mt-auto p-4 text-center text-sm"
|
|
||||||
>
|
|
||||||
<span class="text-gray-600">Powered by </span>
|
|
||||||
<a
|
<a
|
||||||
href="https://www.halo.run"
|
href="https://www.halo.run"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="hover:text-gray-600"
|
class="main-content__footer-link"
|
||||||
>
|
>
|
||||||
Halo
|
Halo
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
<MobileMenu :menus="menus" :minimenus="minimenus" platform="uc" />
|
||||||
<!--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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.navbar {
|
.layout {
|
||||||
@apply w-64;
|
display: flex;
|
||||||
@apply bg-white;
|
min-height: 100vh;
|
||||||
@apply shadow;
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
width: theme("width.64");
|
||||||
|
height: 100%;
|
||||||
|
background-color: theme("colors.white");
|
||||||
|
box-shadow: theme("boxShadow.DEFAULT");
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.profile-placeholder {
|
@media (min-width: theme("screens.md")) {
|
||||||
height: 70px;
|
display: flex;
|
||||||
flex: none;
|
}
|
||||||
|
|
||||||
.current-profile {
|
&__logo-container {
|
||||||
height: 70px;
|
display: flex;
|
||||||
@apply fixed
|
justify-content: center;
|
||||||
bottom-0
|
padding-top: 1.25rem;
|
||||||
left-0
|
padding-bottom: 1.25rem;
|
||||||
flex
|
}
|
||||||
w-64
|
|
||||||
gap-3
|
|
||||||
bg-white
|
|
||||||
p-3;
|
|
||||||
|
|
||||||
.profile-avatar {
|
&__logo {
|
||||||
@apply flex
|
cursor: pointer;
|
||||||
items-center
|
user-select: none;
|
||||||
self-center;
|
transition: all;
|
||||||
}
|
|
||||||
|
|
||||||
.profile-name {
|
&:hover {
|
||||||
@apply flex-1
|
filter: brightness(1.25);
|
||||||
self-center
|
|
||||||
overflow-hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__profile {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.main-content {
|
||||||
@apply ml-0
|
width: 100%;
|
||||||
flex
|
padding-bottom: 3rem;
|
||||||
flex-auto
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
flex-col
|
display: flex;
|
||||||
md:ml-64;
|
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>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue