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