您现在的位置是:首页 >学无止境 >【Vue3+TS项目】硅谷甄选day04--顶部组件搭建+面包屑+路由鉴权网站首页学无止境
【Vue3+TS项目】硅谷甄选day04--顶部组件搭建+面包屑+路由鉴权
顶部组件搭建
顶部左侧折叠和面包屑实现
左侧菜单刷新折叠的问题解决---属性default-active
折叠之后图标不见:icon放在插槽外面----element的menu属性:collapse
projectsrclayoutindex.vue
// 获取路由对象
import { useRoute } from 'vue-router';
let $route = useRoute()
<el-menu :default-active="$route.path" background-color="$base-menu-background" text-color="white"
active-text-color="yellowgreen">
<Menu :menuList="userStore.menuRoutes" />
</el-menu>
顶部tabbar静态组件封装:拆分为左侧面包屑、右侧设置区域
面包屑组件:projectsrclayout abbarreadcrumbindex.vue
<template>
<!-- 顶部左侧静态 -->
<el-icon style="margin-right: 10px;">
<Expand />
</el-icon>
<!-- 左侧面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<el-breadcrumb-item>权限管理</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>
右侧设置组件:projectsrclayout abbarsettingindex.vue
<template>
<el-button size="small" icon="Refresh" circle />
<el-button size="small" icon="FullScreen" circle />
<el-button size="small" icon="Setting" circle />
<img src="/public/logo.png" style="width: 32px;height: 24px;margin: 0 10px;">
<!-- 下拉菜单 -->
<el-dropdown>
<span class="el-dropdown-link">
Admin
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>退出登入</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>
projectsrclayout abbarindex.vue
<template>
<div class="tabbar">
<div class="tabbar_left">
<Breadcrumb />
</div>
<div class="tabbar_right">
<Setting />
</div>
</div>
</template>
<script setup lang="ts">
// 引入组件
import Breadcrumb from '@/layout/tabbar/breadcrumb/index.vue'
import Setting from '@/layout/tabbar/setting/index.vue'
</script>
<script lang="ts">
export default {
name: "Tabbar"
}
</script>
<style scoped lang="scss">
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
.tabbar_left {
display: flex;
align-items: center;
margin-left: 20px;
}
.tabbar_right {
display: flex;
align-items: center;
}
}
</style>
菜单折叠的实现
定义控制折叠展开的变量 fold,放在仓库比较合适,因为其他组件也要根据fold进行变化,放在面包屑组件的传递给其他组件比较麻烦
projectsrcstoremodulessetting.ts
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
const useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //用户控制菜单折叠还是收起控制
}
},
})
export default useLayOutSettingStore
面包屑组件:projectsrclayout abbarreadcrumbindex.vue(通过点击图标切换仓库中fold的状态,其他组件读取时获取最新的)
// 获取小仓库中的控制折叠展开变量 fold
import useLayOutSettingStore from '@/store/modules/setting';
// 获取仓库
let layOutSettingStore = useLayOutSettingStore()
// 点击折叠展开图标的方法
const changeIcon = () => {
layOutSettingStore.fold = !layOutSettingStore.fold
}
<el-icon style="margin-right: 10px;" @click="changeIcon">
<!-- vue提供的动态展示组件 -->
<component :is="layOutSettingStore.fold ? 'Expand' : 'Fold'">
</component>
</el-icon>
layout组件读取 fold 变量
projectsrclayoutindex.vue
// 获取小仓库中的控制折叠展开变量 fold
import useLayOutSettingStore from '@/store/modules/setting';
// 获取仓库
let layOutSettingStore = useLayOutSettingStore()
// :collapse="layOutSettingStore.fold ? true : false"
<el-menu :collapse="layOutSettingStore.fold ? true : false" :default-active="$route.path"
background-color="$base-menu-background" active-text-color="yellowgreen">
<Menu :menuList="userStore.menuRoutes" />
</el-menu>
//顶部导航和右侧内容展示区域添加动态类fold
:class="{ fold: layOutSettingStore.fold ? true : false }"
Collapse 有点问题,折叠的时候还有bug
顶部面包屑动态展示
通过获取路由匹配信息来实现
layout组件不需要展示到面包屑,直接展示首页,所以去掉layout路由中的元信息title和icon,然后再面包屑渲染时进行v-show判断
点击面包屑也可以进行路由跳转--element可以实现---to
projectsrclayout abbarreadcrumbindex.vue
// 获取路由
import { useRoute } from 'vue-router';
let $route = useRoute()
<!-- 左侧面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<el-breadcrumb-item v-for="(item, index) in $route.matched" :key="index" v-show="item.meta.title" :to="item.path">
<!-- 渲染图标 -->
<el-icon style="margin: 0 2px;">
<component :is="item.meta.icon"></component>
</el-icon>
<!-- 渲染面包屑标题 -->
<span>{{ item.meta.title }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
顶部tabbar右侧功能实现
刷新功能实现
简单来说就是路由组件销毁和重建,重新发请求获取数据
涉及顶部导航与内容展示区域间通信---刷新的变量放在小仓库中
小仓库:
refresh: false,//控制刷新效果
setting组件:projectsrclayout abbarsettingindex.vue
// 获取仓库中的 刷新变量 refresh
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
// 点击刷新的回调
const refreshChange = () => {
layOutSettingStore.refresh = !layOutSettingStore.refresh
}
//刷新按钮
<el-button size="small" icon="Refresh" circle @click="refreshChange" />
Main组件监听refresh变化,变化就重新发请求
projectsrclayoutmainindex.vue
import { watch, ref, nextTick } from 'vue'
// 获取仓库中的 刷新变量 refresh
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
// 控制组件销毁与创建
let flag = ref(true)
// 监听仓库中的refresh,如果发生变化说明用户点击刷新按钮
watch(() => layOutSettingStore.refresh, () => {
//refresh变化,则销毁组件
flag.value = false
//等待组件完毕重新加载
nextTick(() => {
flag.value = true
})
})
<component :is="Component" v-if="flag" />
全屏功能实现:dom操作
// 点击实现全屏切换
const fullScreen = () => {
//DOM对象的一个属性:可以用来判断当前是不是全屏模式[全屏:true,不是全屏:false]
let full = document.fullscreenElement;
//切换为全屏模式
if (!full) {
//文档根节点的方法requestFullscreen,实现全屏模式
document.documentElement.requestFullscreen();
} else {
//变为不是全屏模式->退出全屏模式
document.exitFullscreen();
}
}
<el-button size="small" icon="FullScreen" circle @click="fullScreen" />
退出登入业务
token的理解
登入成功,首页组件挂在完毕,通知用户小仓库拿着token去找服务器那用户的相关数据,并存在小仓库中
home组件:projectsrcviewshomeindex.vue(后面可以在路由守卫中发请求)
import { onMounted } from 'vue';
//引入用户相关的仓库,获取当前用户的头像、昵称
import useUserStore from '@/store/modules/user';
//获取存储用户信息的仓库对象
let userStore = useUserStore();
//首页挂在完毕通知小仓库发请求拿数据
onMounted(() => {
userStore.getUserInfo()
})
用户小仓库:projectsrcstoremodulesuser.ts
//记得到type中进行类型定义
username: '',
avatar: ''
// 拿token向服务器请求用户数据
async getUserInfo() {
// 获取用户信息进行存储【头像,用户名】
let res = await reqUserInfo()
if (res.code == 200) {
this.username = res.data.checkUser.username
this.avatar = res.data.checkUser.avatar
} else {
}
}
用户有n个请求,那怎么携带token呢,放在请求拦截器里面
projectsrcutils equest.ts
// 引入用户相关的仓库
import useUserStore from '@/store/modules/user'
// 第二步:request实例添加请求和响应拦截器
request.interceptors.request.use((config) => {
// 获取用户小仓库,拿到登入成功的token数据携带给服务器
let userStore = useUserStore()
if (userStore.token) {
config.headers.token = userStore.token
}
// config 配置对象,有headers属性请求头,经常给服务器端携带公共参数
// 返回配置对象
return config;
})
获取到用户信息之后,在tabbar组件的setting组件中展示一下
projectsrclayout abbarsettingindex.vue
获取仓库,展示即可
退出登入功能实现:点击之后退到登入页,将用户信息和token清除掉
退出登入需要发送请求,告诉服务器token失效,下一次登入服务器重新返回新的token
projectsrclayout abbarsettingindex.vue
// 退出登入需要进行路由跳转
import { useRouter } from 'vue-router';
// 获取路由器对象
let $router = useRouter()
//退出登录点击回调
const logout = async () => {
//第一件事情:需要向服务器发请求[退出登录接口]******暂时没有
//第二件事情:仓库当中关于用于相关的数据清空[token|username|avatar]
//第三件事情:跳转到登录页面
await userStore.userLogout();
//跳转到登录页面
$router.push({ path: '/login' });
}
<el-dropdown-item @click="logout">退出登入</el-dropdown-item>
用户小仓库:projectsrcstoremodulesuser.ts
//退出登入
userLogout() {
//目前没有退出接口
//清除数据
this.token = ''
this.username = ''
this.avatar = ''
localStorage.removeItem('TOKEN')
}
还有一些问题需要解决:登入成功之后不允许在跳转到login、用户信息需要持久化存储
路由鉴权与进度条实现
进度条是可以用全局路由守卫实现(前置,后置)
路由组件挂在完毕我们去请求获取用户信息进行展示,但是不适用大量组件,可以通过全局路由守卫进行实现,路由跳转的时候发请求就可以
创建路由鉴权文件
注意:在组件的外部使用小仓库会报错(同步的语句获取仓库不可以),需要获取小仓库的数据必须先有大仓库
发请求:本来我们是在各个组件中发请求获取用户信息,现在我们可以在前置路由守卫中判断有没有用户名,有的话可以放行到其他路由组件,没有的话先获取用户信息再放行
放在路由守卫中也可以解决我们当时在首页中发请求获取用户信息时,我们跳转其他路由组件数据丢失问题,因为路由守卫就算没有用户信息,也会发请求拿到
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user'
import pinia from './store'
const userStore = useUserStore(pinia)
projectsrcpermisstion.ts
//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router'
import setting from '@/settings'
//@ts-ignore
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'
nprogress.configure({ showSpinner: false })
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user'
import pinia from './store'
const userStore = useUserStore(pinia)
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
//to:你将要访问那个路由
//from:你从来个路由而来
//next:路由的放行函数
nprogress.start()
//获取token,去判断用户登录、还是未登录
const token = userStore.token
//获取用户名字
const username = userStore.username
//用户登录判断
if (token) {
//登录成功,访问login,不能访问,指向首页
if (to.path == '/login') {
next({ path: '/' })
} else {
//登录成功访问其余六个路由(登录排除)
//有用户信息
if (username) {
//放行
next()
} else {
//如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
try {
//获取用户信息
await userStore.getUserInfo()
//放行
//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
next({ ...to })
} catch (error) {
//token过期:获取不到用户信息了
//用户手动修改本地存储token
//退出登录->用户相关的数据清空
await userStore.userLogout()
next({ path: '/login' })
}
}
}
} else {
//用户未登录判断
if (to.path == '/login') {
next()
} else {
next({ path: '/login' })
}
}
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
document.title = `${setting.title} - ${to.meta.title}`
nprogress.done()
})
//第一个问题:任意路由切换实现进度条业务 ---nprogress
//第二个问题:路由鉴权(路由组件访问权限的设置)
//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
//用户未登录:可以访问login,其余六个路由不能访问(指向login)
//用户登录成功:不可以访问login[指向首页],其余的路由可以访问
在main中引入
//引入路由鉴权文件
import './permisstion'