layouts

index

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>

Hamburger

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>

AppMain

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;