mirror of https://github.com/halo-dev/halo
244 lines
5.6 KiB
Vue
244 lines
5.6 KiB
Vue
<script lang="ts" setup>
|
|
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
|
import MenuLoading from "@/components/menu/MenuLoading.vue";
|
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
|
import MobileMenu from "@/layouts/MobileMenu.vue";
|
|
import UserProfileBanner from "@/layouts/UserProfileBanner.vue";
|
|
import { isMac } from "@/utils/device";
|
|
import { coreMenuGroups } from "@console/router/constant";
|
|
import { IconSearch } from "@halo-dev/components";
|
|
import { useEventListener } from "@vueuse/core";
|
|
import {
|
|
useOverlayScrollbars,
|
|
type UseOverlayScrollbarsParams,
|
|
} from "overlayscrollbars-vue";
|
|
import { defineStore } from "pinia";
|
|
import { onMounted, reactive, ref } from "vue";
|
|
import { RouterView, useRoute } from "vue-router";
|
|
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
|
|
|
const route = useRoute();
|
|
|
|
// Global Search
|
|
const globalSearchVisible = ref(false);
|
|
useEventListener(document, "keydown", (e: KeyboardEvent) => {
|
|
const { key, ctrlKey, metaKey } = e;
|
|
if (key === "k" && ((ctrlKey && !isMac) || metaKey)) {
|
|
globalSearchVisible.value = true;
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
const { data, isLoading } = useRouteMenuGenerator(coreMenuGroups);
|
|
|
|
// aside scroll
|
|
const navbarScroller = ref();
|
|
|
|
const useNavbarScrollStore = defineStore("navbar", {
|
|
state: () => ({
|
|
y: 0,
|
|
}),
|
|
});
|
|
|
|
const navbarScrollStore = useNavbarScrollStore();
|
|
|
|
const reactiveParams = reactive<UseOverlayScrollbarsParams>({
|
|
options: {
|
|
scrollbars: {
|
|
autoHide: "scroll",
|
|
autoHideDelay: 600,
|
|
},
|
|
},
|
|
events: {
|
|
scroll: (_, onScrollArgs) => {
|
|
const target = onScrollArgs.target as HTMLElement;
|
|
navbarScrollStore.y = target.scrollTop;
|
|
},
|
|
updated: (instance) => {
|
|
const { viewport } = instance.elements();
|
|
if (!viewport) return;
|
|
viewport.scrollTo({ top: navbarScrollStore.y });
|
|
},
|
|
},
|
|
});
|
|
const [initialize] = useOverlayScrollbars(reactiveParams);
|
|
onMounted(() => {
|
|
if (navbarScroller.value) {
|
|
initialize({ target: navbarScroller.value });
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="layout">
|
|
<aside class="sidebar">
|
|
<div class="sidebar__logo-container">
|
|
<a
|
|
href="/"
|
|
target="_blank"
|
|
:title="$t('core.sidebar.operations.visit_homepage.title')"
|
|
>
|
|
<IconLogo class="sidebar__logo" />
|
|
</a>
|
|
</div>
|
|
<div ref="navbarScroller" class="sidebar__content">
|
|
<div class="sidebar__search-wrapper">
|
|
<div class="sidebar__search" @click="globalSearchVisible = true">
|
|
<span class="sidebar__search-icon">
|
|
<IconSearch />
|
|
</span>
|
|
<span class="sidebar__search-text">
|
|
{{ $t("core.sidebar.search.placeholder") }}
|
|
</span>
|
|
<div class="sidebar__search-shortcut">
|
|
{{ `${isMac ? "⌘" : "Ctrl"}+K` }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<MenuLoading v-if="isLoading" />
|
|
<RoutesMenu v-else :menus="data?.menus || []" />
|
|
</div>
|
|
<div class="sidebar__profile">
|
|
<UserProfileBanner platform="console" />
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main-content">
|
|
<slot v-if="$slots.default" />
|
|
<RouterView v-else />
|
|
<footer v-if="!route.meta.hideFooter" class="main-content__footer">
|
|
<span class="main-content__footer-text">Powered by </span>
|
|
<RouterLink to="/overview" class="main-content__footer-link">
|
|
Halo
|
|
</RouterLink>
|
|
</footer>
|
|
</main>
|
|
<MobileMenu
|
|
:menus="data?.menus || []"
|
|
:minimenus="data?.minimenus || []"
|
|
platform="console"
|
|
/>
|
|
</div>
|
|
<GlobalSearchModal
|
|
v-if="globalSearchVisible"
|
|
@close="globalSearchVisible = false"
|
|
/>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.layout {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.sidebar {
|
|
position: fixed;
|
|
width: theme("width.64");
|
|
height: 100%;
|
|
background-color: theme("colors.white");
|
|
box-shadow: theme("boxShadow.DEFAULT");
|
|
z-index: 999;
|
|
overflow-y: auto;
|
|
display: none;
|
|
flex-direction: column;
|
|
|
|
@media (min-width: theme("screens.md")) {
|
|
display: flex;
|
|
}
|
|
|
|
&__logo-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding-top: 1.25rem;
|
|
padding-bottom: 1.25rem;
|
|
}
|
|
|
|
&__logo {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: all;
|
|
|
|
&:hover {
|
|
filter: brightness(1.25);
|
|
}
|
|
}
|
|
|
|
&__content {
|
|
flex: 1;
|
|
overflow-y: hidden;
|
|
}
|
|
|
|
&__search-wrapper {
|
|
padding-left: 0.75rem;
|
|
padding-right: 0.75rem;
|
|
}
|
|
|
|
&__search {
|
|
display: flex;
|
|
cursor: pointer;
|
|
align-items: center;
|
|
border-radius: 0.25rem;
|
|
background-color: theme("colors.gray.100");
|
|
padding: 0.5rem;
|
|
color: theme("colors.gray.400");
|
|
transition: all;
|
|
|
|
&:hover {
|
|
color: theme("colors.gray.900");
|
|
}
|
|
}
|
|
|
|
&__search-icon {
|
|
margin-right: 0.75rem;
|
|
}
|
|
|
|
&__search-text {
|
|
flex: 1;
|
|
user-select: none;
|
|
font-size: 1rem;
|
|
font-weight: normal;
|
|
}
|
|
|
|
&__search-shortcut {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
&__profile {
|
|
flex: none;
|
|
}
|
|
}
|
|
|
|
.main-content {
|
|
width: 100%;
|
|
padding-bottom: 3rem;
|
|
margin-bottom: env(safe-area-inset-bottom);
|
|
display: flex;
|
|
flex: auto;
|
|
flex-direction: column;
|
|
|
|
@media (min-width: theme("screens.md")) {
|
|
width: calc(100% - 16rem);
|
|
margin-left: theme("width.64");
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
&__footer {
|
|
margin-top: auto;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
&__footer-text {
|
|
color: theme("colors.gray.600");
|
|
}
|
|
|
|
&__footer-link {
|
|
&:hover {
|
|
color: theme("colors.gray.600");
|
|
}
|
|
}
|
|
}
|
|
</style>
|