1<script lang="ts" setup>
2import { computed } from 'vue'
3import { useAppStore } from '@/pinia/stores/app'
4import Sidebar from './components/Sidebar/index.vue'
5import NavigationBar from './components/NavigationBar/index.vue'
6import AppMain from './components/AppMain/index.vue'
7
8const appStore = useAppStore()
9
10const hideSidebar = computed(() => {
11 return {
12 hideSidebar: !appStore.sidebar.opened
13 }
14})
15</script>
16<template>
17 <div :class="hideSidebar" class="layout-wrapper">
18 <!-- 左侧边栏 -->
19 <Sidebar class="layout-sidebar" />
20 <!-- 主容器 -->
21 <div class="layout-main">
22 <!-- 头部导航栏和标签栏 -->
23 <div class="layout-main__header">
24 <NavigationBar />
25 </div>
26 <!-- 页面主体内容 -->
27 <AppMain class="layout-main__content" />
28 </div>
29 </div>
30</template>
31<style lang="scss" scoped>
32.layout-wrapper {
33 width: 100vw;
34 height: 100vh;
35 background-color: $app-background-color;
36
37 .layout-sidebar {
38 background-color: $sidebar-background-color;
39 z-index: 1001;
40 width: $sidebar-max-width;
41 transition: width 0.35s;
42 height: 100vh;
43 position: fixed;
44 top: 0;
45 bottom: 0;
46 left: 0;
47 overflow: hidden;
48 }
49
50 .layout-main {
51 position: relative;
52 min-height: 100vh;
53 margin-left: $sidebar-max-width;
54 transition: margin-left 0.35s;
55
56 &__header {
57 position: relative;
58 z-index: 9;
59 height: $header-height;
60 background-color: $header-background-color;
61 border-bottom: 1px solid red;
62 }
63 &__content {
64 position: relative;
65 min-height: calc(100% - $header-height);
66 overflow: hidden;
67 }
68 }
69}
70
71.hideSidebar {
72 .layout-sidebar {
73 width: $sidebar-min-width;
74 }
75 .layout-main {
76 margin-left: $sidebar-min-width;
77 }
78}
79</style>
1<script lang="ts" setup>
2import { computed } from 'vue'
3import { useAppStore } from '@/pinia/stores/app'
4import Logo from '../Logo/index.vue'
5import Item from './Item.vue'
6import noHiddenRoutes from '@/router/routes.ts'
7
8const appStore = useAppStore()
9const isCollapse = computed(() => !appStore.sidebar.opened)
10
11const activeMenu = '/login'
12</script>
13<template>
14 <div>
15 <Logo :collapse="isCollapse" />
16 <el-scrollbar wrap-class="scrollbar-wrapper">
17 <el-menu
18 :default-active="activeMenu"
19 :collapse="isCollapse"
20 :collapse-transition="false"
21 >
22 <Item
23 v-for="noHiddenRoute in noHiddenRoutes"
24 :key="noHiddenRoute.path"
25 :item="noHiddenRoute"
26 :base-path="noHiddenRoute.path"
27 />
28 </el-menu>
29 </el-scrollbar>
30 </div>
31</template>
32
33<style lang="scss" scoped>
34.el-scrollbar {
35 height: 100%;
36
37 :deep(.scrollbar-wrapper) {
38 // 限制水平宽度
39 overflow-x: hidden;
40 }
41
42 // 滚动条
43 :deep(.el-scrollbar__bar) {
44 &.is-horizontal {
45 // 隐藏水平滚动条
46 display: none;
47 }
48 }
49}
50
51.el-menu {
52 user-select: none;
53 border: none;
54 width: 100%;
55}
56
57:deep(.el-menu-item),
58:deep(.el-sub-menu__title),
59:deep(.el-sub-menu .el-menu-item),
60:deep(.el-menu--horizontal .el-menu-item) {
61 &.is-active,
62 &:hover {
63 background-color: $sidebar-menu-hover-Background-color;
64 }
65}
66</style>
1<script lang="ts" setup>
2import type { RouteRecordRaw } from 'vue-router'
3import { computed } from 'vue'
4interface Props {
5 item: RouteRecordRaw
6}
7
8const props = defineProps<Props>()
9
10/** 是否始终显示根菜单 */
11const isHiddenMenu = computed(() => props.item.meta?.hidden)
12const onlyOne = computed(
13 () => !props.item.children || props.item.children.length == 1
14)
15</script>
16<template>
17 <template v-if="!isHiddenMenu">
18 <template v-if="onlyOne">
19 <router-link :to="props.item.path">
20 <el-menu-item :index="props.item.path">
21 <component :is="props.item?.meta?.elIcon" class="el-icon" />
22 <template v-if="props.item.meta?.title" #title>
23 <span class="title">{{ props.item.meta.title }}</span>
24 </template>
25 </el-menu-item>
26 </router-link>
27 </template>
28 <el-sub-menu v-else :index="props.item.path" teleported>
29 <template #title>
30 <component :is="props.item?.meta?.elIcon" class="el-icon" />
31 <span v-if="props.item.meta?.title" class="title">{{
32 props.item.meta.title
33 }}</span>
34 </template>
35 <template v-if="props.item.children">
36 <Item
37 v-for="child in props.item.children"
38 :key="child.path"
39 :item="child"
40 />
41 </template>
42 </el-sub-menu>
43 </template>
44</template>
:::
1<script lang="ts" setup>
2import { useAppStore } from '@/pinia/stores/app'
3import Hamburger from '../Hamburger/index.vue'
4import Breadcrumb from '../Breadcrumb/index.vue'
5
6const appStore = useAppStore()
7
8function toggleSidebar() {
9 appStore.toggleSidebar(false)
10}
11</script>
12<template>
13 <div class="navigation-bar">
14 <div class="navigation-bar__left">
15 <Hamburger
16 :is-active="appStore.sidebar.opened"
17 class="hamburger"
18 @toggle-click="toggleSidebar"
19 />
20 <Breadcrumb class="breadcrumb" />
21 </div>
22 <div class="navigation-bar__right"></div>
23 </div>
24</template>
25
26<style lang="scss" scoped>
27.navigation-bar {
28 width: 100%;
29 height: $header-height;
30 display: flex;
31 justify-content: space-between;
32 align-items: center;
33 &__left {
34 display: flex;
35 .hamburger {
36 margin: 0 20px;
37 }
38 .breadcrumb {
39 }
40 }
41 &__right {
42 display: flex;
43 }
44}
45</style>
1<script lang="ts" setup>
2import logoText2 from '@/common/assets/images/layouts/logo-text-2.png'
3import logo from '@/common/assets/images/layouts/logo.png'
4import { computed } from 'vue'
5interface Props {
6 collapse?: boolean
7}
8
9const props = withDefaults(defineProps<Props>(), {
10 collapse: true
11})
12
13const logoUrl = computed(() => (props.collapse ? logo : logoText2))
14const logoClass = computed(() =>
15 props.collapse ? 'collapse-logo' : 'expand-logo'
16)
17</script>
18<template>
19 <div class="layout-logo" :class="{ collapse: props.collapse }">
20 <transition name="layout-logo-fade">
21 <router-link to="/">
22 <img :src="logoUrl" :class="logoClass" />
23 </router-link>
24 </transition>
25 </div>
26</template>
27<style lang="scss" scoped>
28.layout-logo {
29 position: relative;
30 width: 100%;
31 height: $header-height;
32 display: flex;
33 align-items: center;
34 justify-content: center;
35 overflow: hidden;
36
37 .collapse-logo {
38 display: none;
39 }
40 .expand-logo {
41 height: 40px;
42 vertical-align: middle;
43 }
44}
45
46.collapse {
47 .collapse-logo {
48 height: 40px;
49 display: inline-block;
50 }
51 .expand-logo {
52 display: none;
53 }
54}
55.layout-logo-fade-enter-active,
56.layout-logo-fade-leave-active {
57 transition: opacity 1.5s;
58}
59.layout-logo-fade-enter-from,
60.layout-logo-fade-leave-to {
61 opacity: 0;
62}
63</style>
1<script lang="ts" setup>
2interface Props {
3 isActive?: boolean
4}
5
6const props = withDefaults(defineProps<Props>(), {
7 isActive: false
8})
9
10const emit = defineEmits<{
11 toggleClick: []
12}>()
13
14function toggleClick() {
15 emit('toggleClick')
16}
17</script>
18
19<template>
20 <div @click="toggleClick">
21 <el-icon :size="20" class="icon">
22 <Fold v-if="props.isActive" />
23 <Expand v-else />
24 </el-icon>
25 </div>
26</template>
27
28<style lang="scss" scoped>
29.icon {
30 color: black;
31}
32</style>
1<script lang="ts" setup>
2const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE
3</script>
4<template>
5 <footer class="layout-footer">© 2024-PRESENT {{ VITE_APP_TITLE }}</footer>
6</template>
7<style lang="scss" scoped>
8.layout-footer {
9 width: 100%;
10 height: $footer-height;
11 display: flex;
12 align-items: center;
13 justify-content: center;
14 color: $footer-color;
15 background-color: $footer-background-color;
16}
17</style>
1<script lang="ts" setup>
2import type { RouteLocationMatched } from 'vue-router'
3import { useRouter } from 'vue-router'
4import { ref } from 'vue'
5// const route = useRoute()
6
7const router = useRouter()
8
9const breadcrumbs = ref<RouteLocationMatched[]>([])
10
11// function getBreadcrumb() {
12// breadcrumbs.value = route.matched.filter(item => item.meta?.title && item.meta?.breadcrumb !== false)
13// }
14
15function handleLink(item: RouteLocationMatched) {
16 const { redirect, path } = item
17 if (redirect) return router.push(redirect as string)
18 router.push(path)
19}
20</script>
21<template>
22 <el-breadcrumb>
23 <el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
24 <span
25 v-if="
26 item.redirect === 'noRedirect' || index === breadcrumbs.length - 1
27 "
28 class="no-redirect"
29 >
30 {{ item.meta.title }}
31 </span>
32 <a v-else @click.prevent="handleLink(item)">
33 {{ item.meta.title }}
34 </a>
35 </el-breadcrumb-item>
36 </el-breadcrumb>
37</template>
1<script lang="ts" setup>
2import Footer from '../Footer/index.vue'
3</script>
4<template>
5 <section class="app-main">
6 <div class="app-scrollbar">
7 <!-- key 采用 route.path 和 route.fullPath 有着不同的效果,大多数时候 path 更通用 -->
8 <router-view v-slot="{ Component, route }">
9 <transition name="el-fade-in" mode="out-in">
10 <keep-alive include="">
11 <component
12 :is="Component"
13 :key="route.path"
14 class="app-container-grow"
15 />
16 </keep-alive>
17 </transition>
18 </router-view>
19 <Footer />
20 </div>
21 </section>
22</template>
1$primary-color: red;
2
3$app-background-color: #fafafa;
4
5$sidebar-background-color: #f5f5f5;
6$sidebar-max-width: 256px;
7$sidebar-min-width: 64px;
8$sidebar-text-color: '#ffffff';
9$sidebar-active-text-color: '#1890ff';
10$sidebar-menu-hover-Background-color: #fff;
11
12$header-background-color: #3f51b5;
13$header-height: 64px;
14
15$footer-height: 80px;
16$footer-color: #fff;
17$footer-background-color: #3f51b5;