Layout 布局容器

<!-- src\layout\AppLayout.vue -->
<template>
  <el-container>
    <el-aside width="200px">
      Aside
    </el-aside>
    <el-container>
      <el-header>Header</el-header>
      <el-main>
        <!-- 子路由出口 -->
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup lang="ts"></script>

<style scoped lang="scss">
.el-container {
  height: 100vh;
}
.el-header {
  background-color: #B3C0D1;
}
.el-aside {
  width: auto;
  background-color: #304156;
}
.el-main {
  background-color: #E9EEF3;
}
</style>

// src\styles\common.scss
* {
  margin: 0;
  padding: 0;
}

// src\router\index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import AppLayout from '@/layout/AppLayout.vue'

const routes:RouteRecordRaw[] = [
  {
    path: '/',
    component: AppLayout,
    children: [
      {
        path: '/',
        name: 'home',
        component: () => import('@/views/home/index.vue')
      }
    ]
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index.vue')
  }
]

const router = createRouter({
  // history: createWebHashHistory(), // hash 路由模式
  history: createWebHistory(), // history 路由模式
  routes // 路由规则
})

export default router

配置页面路由导航

初始化路由目录

创建其他几个页面文件(后面可能还会增加):

└─ product # 商品相关
    ├─ attr # 商品规格
    │   └─ index.vue
    ├─ category # 商品分类
    │   └─ index.vue
    └─ list # 商品列表
        └─ index.vue

配置路由:

// src\router\modules\products.ts
import { RouteRecordRaw, RouterView } from 'vue-router'

const routes:RouteRecordRaw = {
  path: 'product',
  component: RouterView,
  children: [
    {
      path: 'list',
      name: 'product_list',
      component: () => import('@/views/product/list/index.vue')
    },
    {
      path: 'category',
      name: 'product_category',
      component: () => import('@/views/product/category/index.vue')
    },
    {
      path: 'attr',
      name: 'product_attr',
      component: () => import('@/views/product/attr/index.vue')
    },
    {
      path: 'reply',
      name: 'product_reply',
      component: () => import('@/views/product/reply/index.vue')
    }
  ]
}

export default routes

// src\router\index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import AppLayout from '@/layout/AppLayout.vue'
import productRoutes from './modules/product'

const routes:RouteRecordRaw[] = [
  {
    path: '/',
    component: AppLayout,
    children: [
      {
        path: '/',
        name: 'home',
        component: () => import('@/views/home/index.vue')
      },
      productRoutes
    ]
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login/index.vue')
  }
]

const router = createRouter({
  // history: createWebHashHistory(), // hash 路由模式
  history: createWebHistory(), // history 路由模式
  routes // 路由规则
})

export default router

菜单导航

暂时静态编写几个菜单内容:

<!-- src\layout\components\AppMenu.vue -->
<template>
  <el-menu
    active-text-color="#ffd04b"
    background-color="#304156"
    class="el-menu-vertical-demo"
    default-active="2"
    text-color="#fff"
    router
  >
    <el-menu-item index="/">
      <!-- <Menu> 首字母要大写,否则会和浏览器原生的 <menu> 冲突 -->
      <el-icon><Menu /></el-icon>
      <span>首页</span>
    </el-menu-item>
    <el-sub-menu index="1">
      <template #title>
        <el-icon><location /></el-icon>
        <span>商品</span>
      </template>
      <el-menu-item index="/product/list">
        <el-icon><Menu /></el-icon>
        <span>商品列表</span>
      </el-menu-item>
      <el-menu-item index="/product/category">
        <el-icon><Menu /></el-icon>
        <span>商品分类</span>
      </el-menu-item>
      <el-menu-item index="/product/attr">
        <el-icon><Menu /></el-icon>
        <span>商品规格</span>
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<script setup lang="ts"></script>

<style scoped></style>

<!-- src\layout\AppLayout.vue -->
<template>
  <el-container>
    <el-aside width="200px">
      <AppMenu />
    </el-aside>
    <el-container>
      <el-header>Header</el-header>
      <el-main>
        <!-- 子路由出口 -->
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup lang="ts">
import AppMenu from './AppMenu/index.vue'
</script>

<style scoped lang="scss">...</style>

切换侧边栏展开收起

存储侧边栏展开状态:

// src\store\index.ts
import { defineStore } from 'pinia'

const useStore = defineStore('main', {
  state: () => ({
    count: 0,
    isCollapse: false
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

export default useStore

创建 Header 布局组件,编写侧边栏控制按钮:

<!-- src\layout\AppHeader\index.vue -->
<template>
  <ToggleSidebar />

  <!-- 面包屑 -->
</template>

<script setup lang="ts">
import ToggleSidebar from './ToggleSidebar.vue'
</script>

<style scoped lang="scss" >
i {
  font-size: 19px;
  cursor: pointer;
}
</style>

<!-- src\layout\AppHeader\ToggleSidebar.vue -->
<template>
  <el-icon>
    <component
      :is="store.isCollapse ? 'expand' : 'fold'"
      @click="handleCollapse"
    />
  </el-icon>
</template>

<script setup lang="ts">
import useStore from '@/store'

const store = useStore()

// 因为没有其他地方可以修改侧边栏状态
// 所以这里直接修改
const handleCollapse = () => {
  store.isCollapse = !store.isCollapse
}
</script>

<style scoped></style>

绑定侧边栏状态,加载 Header 组件,修改 el-header 样式:

<!-- src\layout\AppLayout.vue -->
<template>
  <el-container>
    <el-aside width="200px">
      <AppMenu />
    </el-aside>
    <el-container>
      <el-header>
        <AppHeader />
      </el-header>
      <el-main>
        <!-- 子路由出口 -->
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup lang="ts">
import AppMenu from './AppMenu/index.vue'
import AppHeader from './AppHeader/index.vue'
</script>

<style scoped lang="scss">
.el-container {
  height: 100vh;
}
.el-header {
  background-color: #fff;
  color: #333;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.el-aside {
  width: auto;
  background-color: #304156;
}
.el-main {
  background-color: #E9EEF3;
}
</style>

面包屑导航

通过路由元信息配置路由标题

// src\router\modules\products.ts
import { RouteRecordRaw, RouterView } from 'vue-router'

const routes:RouteRecordRaw = {
  path: 'product',
  component: RouterView,
  meta: {
    title: '商品'
  },
  children: [
    {
      path: 'list',
      name: 'product_list',
      component: () => import('@/views/product/list/index.vue'),
      meta: {
        title: '商品列表'
      }
    },
    {
      path: 'category',
      name: 'product_category',
      component: () => import('@/views/product/category/index.vue'),
      meta: {
        title: '商品分类'
      }
    },
    {
      path: 'attr',
      name: 'product_attr',
      component: () => import('@/views/product/attr/index.vue'),
      meta: {
        title: '商品规格'
      }
    }
  ]
}

export default routes

// src\router\index.ts
...

const routes:RouteRecordRaw[] = [
  {
    path: '/',
    component: AppLayout,
    children: [
      {
        path: '/',
        name: 'home',
        component: () => import('@/views/home/index.vue'),
        meta: {
          title: '首页'
        }
      },
      ...
    ]
  },
  ...
]

...

面包屑组件

<!-- src\layout\AppHeader\Breadcrumb.vue -->
<template>
  <el-breadcrumb separator-icon="arrow-right">
    <el-breadcrumb-item
      v-for="item in routes"
      :key="item.path"
    >
      {{ item.meta.title }}
    </el-breadcrumb-item>
  </el-breadcrumb>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router'
import { computed } from 'vue'

// 获取路由,类似 Vue2 的 this.$router
const router = useRouter()

// 获取当前路由的匹配记录
const routes = computed(() => {
  return router.currentRoute.value.matched.filter(item => item.meta.title)
})

</script>

<style scoped></style>

加载面包屑组件

<!-- src\layout\AppHeader\index.vue -->
<template>
  <el-space size="large">
    <ToggleSidebar />
    <Breadcrumb />
  </el-space>
</template>

<script setup lang="ts">
import ToggleSidebar from './ToggleSidebar.vue'
import Breadcrumb from './Breadcrumb.vue'
</script>

<style scoped lang="scss" >
i {
  font-size: 19px;
  cursor: pointer;
}
</style>

配置路由元信息 TypeScript 支持,为了方便将自定义创建的类型声明文件放到 src/types 目录下:

// src\types\vue-router.d.ts
import 'vue-router'

declare module 'vue-router' {
  // eslint-disable-next-line no-unused-vars
  interface RouteMeta {
    title?: string
  }
}

其他

可以使用 nuxt/vue-meta(next 分支) 设置页面标题。

全屏功能

全屏 API - Web API 接口参考 | MDN

创建全屏按钮组件:

<!-- src\layout\AppHeader\FullScreen.vue -->
<template>
  <el-icon><full-screen @click="toggleFullScreen" /></el-icon>
</template>

<script setup lang="ts">
const toggleFullScreen = () => {
  if (!document.fullscreenElement) {
    document.documentElement.requestFullscreen()
  } else {
    if (document.exitFullscreen) {
      document.exitFullscreen()
    }
  }
}
</script>

<style scoped></style>

加载组件

<!-- src\layout\AppHeader\index.vue -->
<template>
  <el-space size="large">
    <ToggleSidebar />
    <Breadcrumb />
  </el-space>
  <el-space size="large">
    <FullScreen />
  </el-space>
</template>

<script setup lang="ts">
import ToggleSidebar from './ToggleSidebar.vue'
import Breadcrumb from './Breadcrumb.vue'
import FullScreen from './FullScreen.vue'
</script>

<style scoped lang="scss" >
i {
  font-size: 19px;
  cursor: pointer;
}
</style>

页面加载进度条

使用 nprogress 实现页面加载进度条效果。

npm i --save nprogress
# TS 类型补充模块
npm i --save-dev @types/nprogress
// src\router\index.ts
...
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'

// 关闭 loading 图标
nprogress.configure({ showSpinner: false })

...

router.beforeEach(() => {
  // 开始加载进度条
  nprogress.start()
})

router.afterEach(() => {
  // 结束加载进度条
  nprogress.done()
})

export default router

注意:Vue Router v4.x 开始不建议在导航守卫中使用 next()来调用下一个导航守卫,转而改用 return 来控制,返回 false 会取消当前导航;返回一个路由地址,则会跳转到这个路由;默认路由会调用。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐