一、顶部导航Tabbar(静态页面)
1. 创建顶部导航组件:src\layout\tabbar\index.vue
2. 在layout\index.vue引入顶部导航组件
。。。。。。
<!-- 顶部导航 -->
<div class="layout_navigation">
<Tabbar></Tabbar>
</div>
。。。。。。
//引入顶部导航组件
import Tabbar from "./tabbar/index.vue";
3.编写顶部导航组件 src\layout\tabbar\index.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 "./breadcrumb/index.vue";
//引入顶部导航右侧组件
import Setting from "./setting/index.vue";
</script>
<style lang="scss" >
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
.tabbar-left {
display: flex;
align-items: center;
}
.tabbar-right {
display: flex;
align-items: center;
img {
width: 32px;
height: 32px;
margin: 0 10px;
border-radius: 20px;
}
}
}
</style>
4.编写顶部导航左侧子组件
src\layout\tabbar\breadcrumb\index.vue
<template>
<!-- 顶部左侧的图标 -->
<el-icon style="margin-right: 10px">
<Expand></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>
5.编写顶部导航右侧子组件
src\layout\tabbar\setting\index.vue
<template>
<el-button :icon="Refresh" circle />
<el-button :icon="FullScreen" circle />
<el-button :icon="Setting" circle />
<img src="../../../../public/tx.jpeg" style="width: 24px;height:24px">
<!-- 下拉菜单 -->
<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">
import { ArrowRight ,Refresh,FullScreen,Setting} from '@element-plus/icons-vue'
</script>
<style scoped lang="scss"></style>
效果:
二、菜单折叠
1.折叠变量
定义一个折叠变量来判断现在的状态是否折叠。因为这个变量同时给breadcrumb组件以及父组件layout使用,因此将这个变量定义在pinia中
src\store\modules\setting.ts
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //用户控制菜单折叠还是收起的控制
}
},
})
export default useLayOutSettingStore
layout\index.vue
<template>
<div class="layout_container">
<!-- 左侧菜单 -->
<div class="layout_slier" :class="{ fold: LayOutSettingStore.fold ? true : false }">
<!-- 左侧Logo -->
<Logo></Logo>
<!--展示菜单-->
<!-- 滚动组件 -->
<el-scrollbar class="scrollbar">
<!-- 菜单组件 -->
<!--动态菜单 -->
<el-menu
:collapse="LayOutSettingStore.fold"
:default-active="$route.path"
background-color="#001529"
text-color="white"
unique-opened
>
<Menu :menuList="useStore.menuRoutes"></Menu>
</el-menu>
</el-scrollbar>
</div>
<!-- 顶部导航 -->
<div class="layout_navigation" :class="{ fold: LayOutSettingStore.fold ? true : false }">
<Tabbar></Tabbar>
</div>
<!-- 内容展示区域 -->
<div class="layout_main" :class="{ fold: LayOutSettingStore.fold ? true : false }">
<Main></Main>
</div>
</div>
</template>
<script setup lang="ts">
//引入路由对象
import { useRoute, useRouter } from "vue-router";
//引入左侧logo子组件
import Logo from "./logo/index.vue";
//引入菜单组件
import Menu from "./menu/index.vue";
//引入内容组件
import Main from "./main/index.vue";
//引入顶部导航组件
import Tabbar from "./tabbar/index.vue";
//获取用户相关的小仓库
import useUserStore from "@/store/modules/user";
import useLayOutSettingStore from "@/store/modules/setting";
//获取layout配置相关的仓库
let LayOutSettingStore = useLayOutSettingStore()
let useStore = useUserStore();
//获取路由对象
let $route = useRoute();
</script>
<style scoped lang="scss">
.layout_container {
width: 100%;
height: 100vh;
.layout_slier {
width: $base-menu-width;
height: 100vh;
background: $base-menu-background;
&.fold {
width: $base-menu-min-width;
}
.scrollbar {
width: 100%;
height: calc(100vh - $base-menu-logo-height);
color: white;
.el-menu {
border-right: none; //解决右侧有一条白色的线的问题
}
}
}
.layout_navigation {
//fixed表示固定在一个位置
position: fixed;
top: 0;
left: $base-menu-width;
padding-left: 15px;
width: calc(100% - $base-menu-width);
height: $base-navigation-height;
&.fold {
width: calc(100vw - $base-menu-min-width);
left:$base-menu-min-width;
}
}
.layout_main {
position: absolute;
width: calc(100% - $base-menu-width);
height: calc(100vh - $base-navigation-height);
background: rgb(190, 223, 215);
left: $base-menu-width;
top: $base-navigation-height;
padding-left: 15px;
overflow: auto; //滚动条
&.fold {
width: calc(100vw - $base-menu-min-width);
left:$base-menu-min-width;
}
}
}
</style>
2.面包屑组件
<template>
<el-icon style="margin-right: 10px" @click="changeIcon">
<component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></component>
</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">
import useLayOutSettingStore from '@/store/modules/setting'
//获取layout配置相关的仓库
let LayOutSettingStore = useLayOutSettingStore()
//点击图标的切换
const changeIcon = () => {
//图标进行切换
LayOutSettingStore.fold = !LayOutSettingStore.fold
}
</script>
<style scoped lang="scss"></style>
src\store\modules\setting.ts
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //用户控制菜单折叠还是收起的控制
}
},
})
export default useLayOutSettingStore
三、顶部面包屑动态展示
1.面包屑组件
src\layout\tabbar\breadcrumb\index.vue
<!--
* @Author: huks
* @Date: 2024-03-13 22:35:41
* @LastEditTime: 2024-03-14 00:04:34
* @Description:
-->
<template>
<!-- 顶部左侧的图标 -->
<el-icon style="margin-right: 10px" @click="changeIcon">
<component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></component>
</el-icon>
<!-- 左侧的面包屑 -->
<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>
<component :is="item.meta.icon"></component>
</el-icon>
<!-- 面包屑展示匹配路由的标题 -->
<span>{{ item.meta.title }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
//引入路由
import { useRoute } from 'vue-router';
//引入小仓库layout配置相关的仓库
import useLayOutSettingStore from '@/store/modules/setting'
//获取路由
let $route = useRoute();
//获取layout配置相关的仓库
let LayOutSettingStore = useLayOutSettingStore()
//点击图标的切换
const changeIcon = () => {
//图标进行切换
LayOutSettingStore.fold = !LayOutSettingStore.fold
}
</script>
<style scoped lang="scss"></style>
效果图:
注意:要想面包屑不显示layout,只需把item.meta.title和item.meta.icon改为空
重定向到第一个子组件
四、刷新业务
1.src\store\modules\setting.ts
2.src\layout\tabbar\setting\index.vue
<template>
<el-button :icon="Refresh" circle @click="updateRefresh"/>
<el-button :icon="FullScreen" circle />
<el-button :icon="Setting" circle />
<img src="../../../../public/tx.jpeg" style="width: 24px; height: 24px" />
<!-- 下拉菜单 -->
<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">
import { Refresh, FullScreen, Setting } from "@element-plus/icons-vue";
import useLayOutSettingStore from "@/store/modules/setting"
let layoutSettingStore = useLayOutSettingStore();
//刷新按钮的回调
const updateRefresh=()=>{
layoutSettingStore.refsh = !layoutSettingStore.refsh
}
</script>
<style scoped lang="scss"></style>
3.src\layout\main\index.vue
<template>
<!-- 路由组件出口的位置 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染layout一级路由的子路由 -->
<component :is="Component" v-if="flag" />
</transition>
</router-view>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from "vue";
//使用layout的小仓库
import useLayOutSettingStore from "@/store/modules/setting";
let layoutSettingStore = useLayOutSettingStore();
//控制当前组件是否销毁重建
let flag = ref(true);
//监听仓库内部的数据是否发生改变,如果发生变化,说明用户点击过刷新按钮
watch(
() => layoutSettingStore.refsh,
() => {
//点击刷新按钮:路由组件销毁
flag.value = false;
nextTick(() => {
flag.value = true;
});
}
);
</script>
<style lang="scss" scoped>
.fade-enter-from {
opacity: 0;
}
.fade-enter-active {
transition: all 0.3s;
}
.fade-enter-to {
opacity: 1;
}
</style>
五、全屏模式
src\layout\tabbar\setting\index.vue
<template>
<el-button :icon="Refresh" circle @click="updateRefresh" />
<el-button :icon="FullScreen" circle @click="fullScreen" />
<el-button :icon="Setting" circle />
<img src="../../../../public/tx.jpeg" style="width: 24px; height: 24px" />
<!-- 下拉菜单 -->
<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">
import { Refresh, FullScreen, Setting } from "@element-plus/icons-vue";
import useLayOutSettingStore from "@/store/modules/setting";
let layoutSettingStore = useLayOutSettingStore();
//刷新按钮的回调
const updateRefresh = () => {
layoutSettingStore.refsh = !layoutSettingStore.refsh;
};
//全屏模式的回调
//全屏按钮点击的回调
const fullScreen = () => {
//DOM对象的一个属性:可以用来判断当前是不是全屏的模式【全屏:true,不是全屏:false】
let full = document.fullscreenElement
//切换成全屏
if (!full) {
//文档根节点的方法requestFullscreen实现全屏
document.documentElement.requestFullscreen()
} else {
//退出全屏
document.exitFullscreen()
}
}
</script>
<style scoped lang="scss"></style>
六、登录获取用户信息(TOKEN)
登录之后页面(home)上来就要获取用户信息。并且将它使用到页面中
1.home组件挂载获取用户信息
src\views\home\index.vue
<script setup lang="ts">
//引入组合式API生命周期函数
import { onMounted } from "vue";
//获取仓库
import useUserStore from "@/store/modules/user";
let userStore = useUserStore();
onMounted(() => {
userStore.userInfo();
});
</script>
2.小仓库中定义用户信息以及type声明
src\store\modules\user.ts
src\store\modules\types\type.ts
import type { RouteRecordRaw } from "vue-router";
//定义小仓库数据state类型
export interface UserState{
token: string | null;
menuRoutes: RouteRecordRaw[];
username: string;
avatar: string
}
3.请求头添加TOKEN
src\utils\request.ts
//引入用户相关的仓库
import useUserStore from '@/store/modules/user'
。。。。。。
//第二步:request实例添加请求与响应拦截器
request.interceptors.request.use(config => {
//获取用户相关的小仓库,获取token,登录成功以后携带个i服务器
const userStore = useUserStore()
if (userStore.token) {
config.headers.token = userStore.token
}
//config配置对象:headers属性请求头,经常给服务器携带公共参数
//返回配置对象
return config;
});
4.小仓库发请求并且拿到用户信息
src\store\modules\user.ts
//获取用户信息方法
async userInfo() {
//获取用户信息进行存储
let result = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.checkUser.username
this.avatar = result.data.checkUser.avatar
}
},
5.更新tabbar的信息(记得先引入并创建实例)
src\layout\tabbar\setting\index.vue
//引入用户相关的仓库
import useUserStore from "@/store/modules/user";
let userStore = useUserStore();
七、退出功能
7.1.退出登录绑定函数,调用仓库函数:
src\layout\tabbar\setting\index.vue
//退出登陆点击的回调
const logout = () => {
//第一件事:需要项服务器发请求【退出登录接口】(我们这里没有)
//第二件事:仓库当中和关于用户的相关的数据清空
userStore.userLogout()
//第三件事:跳转到登陆页面
$router.push({ path: '/login', query: { redirect: $route.path } })
}
7.2pinia仓库
src\store\modules\user.ts
//退出登录
userLogout() {
//当前没有mock接口(不做):服务器数据token失效
//本地数据清空
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
},
7.3 退出登录,路由跳转
注意:携带的query参数方便下次登陆时直接跳转到当时推出的界面
个人觉得这个功能没什么作用,因为一般都是跳到首页
src\layout\tabbar\setting\index.vue
//退出登陆点击的回调
const logout = () => {
//第一件事:需要项服务器发请求【退出登录接口】(我们这里没有)
//第二件事:仓库当中和关于用户的相关的数据清空
userStore.userLogout()
//第三件事:跳转到登陆页面
$router.push({ path: '/login', query: { redirect: $route.path } })
}
7.4 登录页面进行判断
src\views\login\index.vue
八、路由鉴权
1.进度条
1.1.在入口文件main.ts引入
1.2.安装进度条
pnpm i nprogress
1.3.引入并使用
src\permission.ts
//路由鉴权:鉴权:项目当中路由能不能被访问的权限
import router from '@/router'
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'
//全局前置守卫
router.beforeEach((to: any, from: any, next: any) => {
//访问某一个路由之前的守卫
nprogress.start()
next()
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
// to and from are both route objects.
nprogress.done()
})
//第一个问题:任意路由切换实现进度条业务 ----nprogress
效果图:
2.路由鉴权
src\permission.ts
//路由鉴权:鉴权:项目当中路由能不能被访问的权限
import router from '@/router'
import setting from './setting'
import nprogress from 'nprogress'
//引入进度条样式
import 'nprogress/nprogress.css'
//进度条的加载圆圈不要
nprogress.configure({ showSpinner: false })
//获取用户相关的小仓库内部token数据,去判断用户是否登陆成功
import useUserStore from './store/modules/user'
//为什么要引pinia
import pinia from './store'
const userStore = useUserStore(pinia)
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
//网页的名字
document.title = `${setting.title}-${to.meta.title}`
//访问某一个路由之前的守卫
nprogress.start()
//获取token,去判断用户登录、还是未登录
const token = userStore.token
//获取用户名字
let username = userStore.username
//用户登录判断
if (token) {
//登陆成功,访问login。指向首页
if (to.path == '/login') {
next('/home')
} else {
//登陆成功访问其余的,放行
//有用户信息
if (username) {
//放行
next()
} else {
//如果没有用户信息,在收尾这里发请求获取到了用户信息再放行
try {
//获取用户信息
await userStore.userInfo()
next()
} catch (error) {
//token过期|用户手动处理token
//退出登陆->用户相关的数据清空
userStore.userLogout()
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
//用户未登录
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
next()
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
// to and from are both route objects.
nprogress.done()
})
//第一个问题:任意路由切换实现进度条业务 ----nprogress
//第二个问题:路由鉴权
//全部路由组件 :登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(4个子路由)
//用户未登录 :可以访问login 其余都不行
//登陆成功:不可以访问login 其余都可以
路由鉴权几个注意点:
- 获取用户小仓库为什么要导入pinia?
- 个人理解:之前在app中是不需要导入pinia的,是因为我们这次的文件时写在和main.ts同级的下面,所以我们使用的时候是没有pinia的。而之前使用时app已经使用了pinia了,所以我们不需要导入pina。
- 全局路由守卫将获取用户信息的请求放在了跳转之前。实现了刷新后用户信息丢失的功能。
九、 真实接口替代mock接口
接口文档:
9.1修改服务器域名
将.env.development,.env.production .env.test,三个环境文件下的服务器域名写为:
9.2代理跨域
vite.config.ts
import { loadEnv } from 'vite'
。。。。。。
export default defineConfig(({ command, mode }) => {
//获取各种环境下的对应的变量
let env = loadEnv(mode, process.cwd())
return {
。。。。。。。
//代理跨域
server: {
proxy: {
[env.VITE_APP_BASE_API]: {
//获取数据服务器地址的设置
target: env.VITE_SERVE,
//需要代理跨域
changeOrigin: true,
//路径重写
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
}
})
9.3修改api
在这里退出登录有了自己的api
src\api\user\index.ts
//统一管理咱们项目用户相关的接口
import request from '@/utils/request'
import type {
loginForm,
loginResponseData,
userResponseData,
} from './type'
//项目用户相关的请求地址
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout',
}
//登录接口
export const reqLogin = (data: loginForm) =>
request.post<any, loginResponseData>(API.LOGIN_URL, data)
//获取用户信息
export const reqUserInfo = () =>
request.get<any, userResponseData>(API.USERINFO_URL)
//退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
9.4小仓库(user)
替换原有的请求接口函数,以及修改退出登录函数。以及之前引入的类型显示我们展示都设置为any
src\store\modules\user.ts
//创建用户相关的小仓库
import { defineStore } from 'pinia'
//引入接口
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user'
import type { UserState } from './types/type'
//引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
//创建用户小仓库
const useUserStore = defineStore('User', {
//小仓库存储数据地方
state: (): UserState => {
return {
token: GET_TOKEN(), //用户唯一标识token
menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)
username: '',
avatar: '',
}
},
//处理异步|逻辑地方
actions: {
//用户登录的方法
async userLogin(data: any) {
//登录请求
const result: any = await reqLogin(data)
if (result.code == 200) {
//pinia仓库存储token
//由于pinia|vuex存储数据其实利用js对象
this.token = result.data as string
//本地存储持久化存储一份
SET_TOKEN(result.data as string)
//保证当前async函数返回一个成功的promise函数
return 'ok'
} else {
return Promise.reject(new Error(result.data))
}
},
//获取用户信息方法
async userInfo() {
//获取用户信息进行存储
const result = await reqUserInfo()
console.log(result)
if (result.code == 200) {
this.username = result.data.name
this.avatar = result.data.avatar
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
//退出登录
async userLogout() {
const result = await reqLogout()
if (result.code == 200) {
//本地数据清空
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
},
},
getters: {},
})
//对外暴露小仓库
export default useUserStore
9.5 退出登录按钮修改
src\layout\tabbar\setting\index.vue
9.6 路由跳转判断条件修改
src\permission.ts
十、接口类型的定义
src\api\user\type.ts
/*
* @Author: huks
* @Date: 2024-03-06 00:52:09
* @LastEditTime: 2024-03-17 00:53:49
* @Description:
*/
//登录接口需要携带参数类型
export interface loginFormData {
username: string
password: string
}
//定义全部接口返回数据都有的数据类型
export interface ResponseData {
code: number
message: string
ok: boolean
}
//定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {
data: string
}
//定义获取用户信息返回的数据类型
export interface userInfoResponseData extends ResponseData {
data: {
routes: string[]
button: string[]
roles: string[]
name: string
avatar: string
}
}
注意:在src\store\modules\user.ts以及src\api\user\index.ts文件中对发请求时的参数以及返回的数据添加类型定义