【Vue3+Ts项目】硅谷甄选 — 路由配置+登录模块+layout组件+路由鉴权

一、路由配置

项目一共需要4个一级路由:登录(login)、主页(home)、404、任意路由(重定向到404)。

1.1 安装路由插件

pnpm install vue-router

1.2 创建路由组件 

在src目录下新建views文件夹,在views中创建login、home、404路由组件。

1.3 配置路由

在src目录下新建router文件夹,书写路由配置(包含index.ts和routes.ts)。

src/router/routes.ts

// 对外暴露配置路由(常量路由)
export const constantRoute = [{// 登录path: '/login',component: () => import('@/views/login/index.vue'),name: 'login'},{// 登录成功以后展示数据的路由path: '/',component: () => import('@/views/home/index.vue'),name: 'layout'},{// 404path: '/404',component: () => import('@/views/404/index.vue'),name: '404'},{// 任意路由path: '/:pathMatch(.*)*',redirect: '/404',name: 'Any'}
]

src/router/index.ts

// 通过vue-router插件实现路由配置
import { createRouter, createWebHashHistory } from 'vue-router';
// 引入routes配置项
import { constantRoute } from './routes';
// 创建路由
let router = createRouter({// 路由模式hashhistory: createWebHashHistory(),routes: constantRoute,// 滚动行为scrollBehavior() {return {left: 0,top: 0}}
})export default router;

1.4 引入路由

在入口文件(main.js)引入路由:

// 引入路由
import router from '@/router'
// 注册模板路由
app.use(router)

最后,在模板中通过 <router-view></router-view>占位,根据当前的路由状态动态地渲染匹配到的组件。

二、登录模块

 2.1 登录路由静态的搭建

采用element-plus中的Layout布局(栅格布局)、From表单组件、input组件、button组件。

Layout布局:一共是24 分栏,:span代表栅格占据的列数,:xs代表屏幕宽度<768px时栅格占据的列数。

input组件::prefix-icon代表前缀图标,show-password代表是否显示切换密码图标

src/views/login/index.vue

<template><div class="login_container"><el-row><el-col :span="12" :xs="0"></el-col><el-col :span="12" :xs="24"><el-form class="login_from"><h1>Hello</h1><h2>欢迎来到唧唧bong甄选</h2><el-form-item><el-input :prefix-icon="User" v-model="loginFrom.username"></el-input></el-form-item><el-form-item><el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input></el-form-item><el-form-item><el-button class="login_btn" type="primary" size="default">登录</el-button></el-form-item></el-form></el-col></el-row></div>
</template><script setup lang="ts">
import {User, Lock} from '@element-plus/icons-vue'
import { reactive } from 'vue';
let loginFrom = reactive({username: 'admin',password: '111111'
})
</script><style scoped lang="scss">
.login_container {width: 100%;height: 100vh;background: url('@/assets/images/background.jpg') no-repeat;background-size: cover;.login_from{width: 80%;position: relative;top: 30vh;background: url('@/assets/images/login_form.png') no-repeat;background-size: cover;padding: 40px;h1{color: white;font-size: 40px;}h2{color: white;font-size: 20px;margin: 20px 0;}.login_btn{width: 100%;}}
}
</style>

2.2 模板封装登录业务

点击登录时,会携带用户名和密码向服务器发请求获取token,此时我们需要把token存储起来,用于后续向服务端发请求获取信息的身份验证,这里我们用pinia和loacalStroage进行存储。

安装pinia

pnpm i pinia

创建大仓库:src/store/index.ts

import { createPinia } from 'pinia'
//创建大仓库
const pinia = createPinia()
//对外暴露:入口文件需要安装仓库
export default pinia

在入口文件(main.ts)中引入并安装:src/main.ts

// 引入大仓库
import pinia from './store'
// 安装仓库
app.use(pinia)

创建小仓库:src/store/modules/user.ts

import { defineStore } from 'pinia'
// 引入接口
import { reqLogin } from '@/api/user'
// 引入类型
import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from './types/type'
// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN } from '@/utils/token'
// 创建用户小仓库
const useUserStore = defineStore('User', {// 小仓库存储数据的地方state: (): UserState => {return {token: GET_TOKEN(), //用户唯一的标识token}},// 异步|逻辑的地方actions: {// 用户登录的方法async userLogin(data: loginForm) {// 登录请求let result: loginResponseData = await reqLogin(data)// 登录请求:成功200->token// 登录请求:失败201->登录失败错误的信息if (result.code === 200) {// pinia仓库存储一下token// 由于pinia|vuex存储数据其实利用js对象(非持久化存储)this.token = (result.data.token as string)// 本地化持久存储一份SET_TOKEN((result.data.token as string))// 能保证当前async函数返回一个成功的promisereturn 'ok'}else {return Promise.reject(new Error(result.data.message))}}},getters: {}
})// 对外暴露用户小仓库
export default useUserStore

在登录页面中引入小仓库,点击登录时通知user小仓库发请求,存储token:src/views/login/index.vue

<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router'
// 引入element-plus提示框
import { ElNotification } from 'element-plus'
// 引入用户相关的小仓库
import useUserStore from '@/store/modules/user'
let useStore = useUserStore()// 获取路由
let $router = useRouter()
// 定义变量控制按钮加载效果
let loading = ref(false)
// 收集账号与密码的数据
let loginFrom = reactive({username: 'admin',password: '111111'
})
const login = async () => {// 加载效果:开始加载loading.value = true//点击登录按钮以后干什么?//通知仓库发登录请求//请求成功->首页展示数据的地方//请求失败->弹出登录失败信息try {// 保证登录成功await useStore.userLogin(loginFrom)// 编程式导航跳转到展示数据首页$router.push('/')// 登录成功信息提示ElNotification({type: 'success',message: '登录成功',})// 登录成功加载效果消失loading.value = false} catch (error) {// 登录失败加载效果消失loading.value = false// 登录失败的提示信息ElNotification({type: 'error',message: (error as Error).message})}
}
</script>
  • userLogin会返回一个Promise,此处可以使用try...catch...或.then来进行下一步结果处理。
  • 不管成功或失败,都需要使登录加载效果消失,因此也可以统一写在finally里面:
try {// 保证登录成功await useStore.userLogin(loginFrom)// 编程式导航跳转到展示数据首页$router.push('/')// 登录成功信息提示ElNotification({type: 'success',message: '登录成功',}) } catch (error) {// 登录失败的提示信息ElNotification({type: 'error',message: (error as Error).message})} finally{// 登录成功/失败加载效果消失loading.value = false}

2.3 用户仓库数据ts类型的定义

定义小仓库数据state类型:src\store\modules\types\type.ts 

// 定义小仓库数据state类型
export interface UserState {token: string | null
}

 登录接口返回的数据类型:src\api\user\type.ts 

登录请求可能返回成功/失败的数据,因此类型需要dataType需要包括成功的数据token和失败的数据message,且是可选的,要加上"?"。

interface dataType {token?: string,message?:string
}// 登录接口返回的数据类型
export interface loginResponseData {code: number,data: dataType
}

封装本地存储数据和读取方法:src/utils/token.js

// 存储数据
export const SET_TOKEN = (token: string) => {localStorage.setItem('TOKEN', token)
}// 本地存储获取数据
export const GET_TOKEN = () => {return localStorage.getItem('TOKEN')
}

2.4 登录时间的判断与封装

在utils中封装一个函数:src/utils/time.js

// 封装一个函数:获取一个结果:当前早上|上午|中午|下午|晚上
export const getTime = () => {let time = ''// 通过内置的构造函数Datelet hour = new Date().getHours()if (hour < 9) {time = '早上'}else if (hour <= 12) {time = '上午'}else if (hour <= 14) {time = '中午'}else if (hour <= 18) {time = '下午'}else {time = '晚上'}return time
}

 在login组件中引入并使用

// 引入当前时间的函数
import { getTime } from '@/utils/time'
......// 登录成功信息提示
ElNotification({type: 'success',message: '欢迎回来',title: `HI,${getTime()}好`
})

2.5 登录模块表单校验

使用element-plus的表单验证功能 ,步骤如下:

  1. 给el-form添加 :model="loginFrom"和:rules="rules"
  2. 给需要验证的每个el-form-item添加prop属性,如 prop="username"、prop="password"
  3. 定义表单校验需要配置对象rules
  4. 请求前使用 loginFroms.value.validate()触发表单中所有表单项的校验,保证全部的表单项校验通过再发请求
  • :model:要验证的表单数据对象
  • :rules="rules":表单验证规则
  • prop:要校验字段的属性名
// 第一步:给el-form添加 :model="loginFrom"和:rules="rules"
<el-form class="login_form" :model="loginForm" :rules="rules" ref="loginFroms">// 第二步:给需要验证的每个el-form-item添加prop属性,如 prop="username"、prop="password"
<el-form-item prop="username"><el-input :prefix-icon="User" v-model="loginFrom.username"></el-input>
</el-form-item>
<el-form-item prop="password"><el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input>
</el-form-item>// 第三步:定义表单校验需要配置对象rules
// 规则对象属性:
// required:代表这个字段必须校验
// min:文本长度至少多少位
// max:文本长度最多多少位
// message:错误的提示信息
// trigger:触发校验表单的时机,change:文本发生变化时触发校验,blur:失去焦点时触发校验
const rules = {username: [{ required: true, min: 5, max: 10, message: '用户名长度应为5-10位', trigger: 'change' },],password: [{ required: true, min: 6, max: 10, message: '密码长度应为6-10位', trigger: 'change' },
​]
}第四步:请求前使用 loginFroms.value.validate()触发表单中所有表单项的校验,保证全部的表单项校验通过再发请求
// 通过ref属性获取el-form组件
let loginFroms = ref()
const login = async () => {// 保证全部的表单项校验通过再发请求await loginForms.value.validate()......
}

PS:在 el-form 组件中,可以使用 ref 属性来获取表单的引用,然后调用该引用上的 validate 方法。这个方法会触发表单中所有表单项的校验,并返回一个 Promise 对象,该对象的 resolve 回调函数会在校验通过时被调用,而 reject 回调函数会在校验失败时被调用。

 2.6 自定义校验表单

上面的验证比较简单,公司的开发项目中表单验证会更复杂,这个时候就要用到element-plus的自定义校验规则了 。

自定义校验表单的配置项中需要一个validator属性,值是一个方法,用于书写自定义规则。

// 自定义校验规则函数
const validateUsername = (rule: any, value: any, callback: any) => {//rule:即为校验规则对象//value:即为表单元素文本内容//函数:如果符合条件callback放行通过即为//如果不符合条件callback方法,注入错误提示信息if(value.length >= 5){callback()}else{callback(new Error('用户名不少于5位'))}
}const validatePassword = (rule: any, value: any, callback: any) => {if(value.length >= 6){callback()}else{callback(new Error('用户名不少于6位'))}
}// 定义表单校验需要配置对象
const rules = {username: [{ validator: validateUsername, trigger: 'change' },],password: [{ validator: validatePassword, trigger: 'change' },]
}

PS:这里只是简单的示范,正式开发中大多场景的校验规则会更复杂,需要用到正则表达式来书写。

三、layout组件 

3.1 layout组件的静态搭建 

layout组件主页分为三部分:左侧菜单、顶部导航、内容展示区域。

在src目录下创建layout组件:src/layout/index.vue

<template><div class="layout_container"><!-- 左侧菜单 --><div class="layout_slider">左侧菜单</div><!-- 顶部导航 --><div class="layout_tabbar">顶部导航</div><!-- 内容展示区域 --><div class="layout_main"><p style="height: 10000px;background: red;">内容</p></div></div>
</template><script setup lang="ts"></script><style scoped lang="scss">
.layout_container {width: 100%;height: 100vh;.layout_slider {width: $base-menu-width;height: 100vh;background: $base-menu-background;}.layout_tabbar {position: fixed;width: calc(100% - $base-menu-width);height: $base-tabbar-height;background: $base-tabbar-background;top: 0px;left: $base-menu-width;}.layout_main {position: absolute;width: calc(100% - $base-menu-width);height: calc(100vh - $base-tabbar-height);background: $base-main-background;top: $base-tabbar-height;left: $base-menu-width;overflow: auto;padding: 20px;}
}
</style>

配置layout相关的样式的全局变量:src/styles/variable.scss

// 左侧菜单的宽度
$base-menu-width: 260px;
// 左侧菜单的背景颜色
$base-menu-background: #001529;
// 顶部导航的高度
$base-tabbar-height: 50px;
// 顶部导航的背景颜色
$base-tabbar-background: #ffffff;
// 内容展示区域的背景颜色
$base-main-background: #ccc8cc;

 设置滚动条样式:src/styles/index.scss

// 滚动条外观设置
::-webkit-scrollbar{width: 10px;
}::-webkit-scrollbar-track{background: $base-menu-background;
}::-webkit-scrollbar-thumb{width: 10px;background: yellowgreen;border-radius: 10px;
}

3.2 Logo组件的封装

创建logo组件:src/layout/logo/index.vue

<template><div class="logo" v-if="setting.logoHidden"><img :src="setting.logo" alt=""><p>{{ setting.title }}</p></div>
</template><script setup lang="ts">
//引入设置标题与logo这配置文件
import setting from '@/setting'
</script><style scoped lang="scss">
.logo {width: 100%;height: $base-menu-logo-height;color: white;display: flex;align-items: center;padding: 10px;img {width: 40px;height: 40px;}p {font-size: $base-logo-title-fontSize;margin-left: 10px;}
}
</style>

配置logo相关的样式的全局变量:src/styles/variable.scss

//左侧菜单logo高度设置
$base-menu-logo-height:50px;//左侧菜单logo右侧文字大小
$base-logo-title-fontSize:16px;

项目logo/标题配置文件:src/setting.ts

// 用于项目logo|标题配置
export default {title:'唧唧bong甄选运营平台', // 项目标题logo:'/logo.png', // 项目logo设置logoHidden: true // logo组件是否隐藏设置
}

3.3 左侧菜单组件

3.3.1 递归组件生成动态菜单

创建menu组件:src/layout/menu/index.vue,并在layout中引入并使用menu组件

添加二级路由:src/router/routes.ts

 {// 登录成功以后展示数据的路由path: '/',component: () => import('@/layout/index.vue'),name: 'layout',meta: {title: 'layout',hidden: true },children: [{path: '/home',component: () => import('@/views/home/index.vue'),name: 'home',meta: {title: '首页',hidden: false }}]},

将路由数组存储到store中(方便组件访问路由数据):src/store/modules/user.ts

// 引入路由(常量路由)
import { constantRoute } from '@/router/routes';
// 创建用户小仓库
const useUserStore = defineStore('User', {// 小仓库存储数据的地方state: (): UserState => {return {token: GET_TOKEN(), //用户唯一的标识token//路由配置数据menuRoutes: constantRoute}},......
})

UserState中添加路由的类型定义:src/store/modules/types/type.ts

// 引入描述路由配置信息的类型(这个类型包含了路由的路径、组件、子路由等信息)
import type { RouteRecordRaw } from 'vue-router'
// 定义小仓库数据state类型
export interface UserState {token: string | null,menuRoutes: RouteRecordRaw[]
}

layout组件中引入小仓库获取路由数据,通过props传递给menu组件:src/layout/index.vue

<template><div class="layout_container"><!-- 左侧菜单 --><div class="layout_slider"><Logo></Logo><!-- 展示菜单 --><el-scrollbar class="scrollbar"><el-menu background-color="#001529" text-color="white"><!-- 传递路由数据给menu组件 --><Menu :menuList="userStore.menuRoutes"></Menu></el-menu></el-scrollbar></div>......</div>
</template><script setup lang="ts">
// 引入菜单组件
import Menu from './menu/index.vue'// 获取用户相关的小仓库
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
</script>

给每个路由添加meta元信息:src/router/routes.ts

meta: {title: '登录', // 菜单标题hidden: true // 代表路由标题在菜单中是否隐藏  true:隐藏  false:显示}

书写menu组件:src/layout/menu/index.vue

1. menu组件分三种情况:

  • 没有子路由
  • 有且只有一个子路由
  • 有一个以上的子路由

2. 点击菜单item跳转路由(@click="goRoute"):

  • <el-menu-item>标签有click事件(菜单点击时的回调函数,回调参数是el-menu-item实例。
<template><template v-for="(item, index) in menuList" :key="item.path"><!-- 没有子路由 --><template v-if="!item.children"><el-menu-item :index="item.path" v-if="!item.meta.hidden" @click="goRoute"><template #title><el-icon><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></template></el-menu-item></template><!-- 有且只有一个子路由 --><template v-if="item.children && item.children.length == 1"><el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.hidden" @click="goRoute"><template #title><el-icon><component :is="item.children[0].meta.icon"></component></el-icon><span>{{ item.children[0].meta.title }}</span></template></el-menu-item></template><!-- 有子路由,且个数大于一 --><el-sub-menu v-if="item.children && item.children.length > 1" :index="item.path"><template #title><el-icon><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></template><Menu :menuList="item.children"></Menu></el-sub-menu></template>
</template><script setup lang="ts">
import { useRouter } from "vue-router";
// 获取父组件传递过来的全部路由数组
defineProps(['menuList'])
// 获取路由对象
let $router = useRouter()
// 点击菜单的回调
const goRoute = (vc: any) => {// 路由跳转$router.push(vc.index)
}
</script>
<script lang="ts">
export default {name: 'Menu'
}
</script><style scoped></style>

PS递归组件必须有一个名字,因为在vue中,组件是通过其名字进行注册和引用的。递归组件需要在自身的模板中引用自身,但如果组件没有名字,Vue无法在模板中正确地引用它,从而导致递归出现问题。

 3.3.2 菜单图标完成

将element-plus图标 注册成全局组件:src/components/index.ts

具体可参考官网:Icon 图标 | Element Plus (gitee.io)

// 引入elemnet-plus提供全部图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'// 对外暴露一个插件对象
export default {install(app: any) {// 将element-plus提供图标注册为全局组件for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)}}
}

菜单图标由路由配置决定,meta中添加 icon 字段:src/router/routes.ts

meta: {......icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标}

在menu中使用element-plus图标:src/layout/menu/index.vue

<el-icon><component :is="item.meta.icon"></component>
</el-icon>

3.3.3 项目全部路由配置

  • 首页重定向到home
  • 权限管理和商品管理的一级路由用的还是组件 layout

src/router/routes.ts 

// 对外暴露配置路由(常量路由)
export const constantRoute = [{// 登录path: '/login',component: () => import('@/views/login/index.vue'),name: 'login',meta: {title: '登录', // 菜单标题hidden: true, // 代表路由标题在菜单中是否隐藏  true:隐藏  false:显示icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标}},{// 登录成功以后展示数据的路由path: '/',component: () => import('@/layout/index.vue'),name: 'layout',meta: {title: 'layout',hidden: true,icon: 'Avatar',},redirect: '/home',children: [{path: '/home',component: () => import('@/views/home/index.vue'),name: 'home',meta: {title: '首页',hidden: false,icon: 'HomeFilled',}}]},{// 404path: '/404',component: () => import('@/views/404/index.vue'),name: '404',meta: {title: '404',hidden: true,icon: 'BrushFilled',}},{path: '/screen',component: () => import('@/views/screen/index.vue'),name: 'Screen',meta: {title: '数据大屏',hidden: false,icon: 'Platform',}},{path: '/acl',component: () => import('@/layout/index.vue'),name: 'Acl',meta: {title: '权限管理',icon: 'Lock',},children: [{path: '/acl/user',component: () => import('@/views/acl/user/index.vue'),name: 'User',meta: {title: '用户管理',icon: 'User',}},{path: '/acl/role',component: () => import('@/views/acl/role/index.vue'),name: 'Role',meta: {title: '角色管理',icon: 'UserFilled',}},{path: '/acl/permission',component: () => import('@/views/acl/permission/index.vue'),name: 'Permission',meta: {title: '菜单管理',icon: 'Monitor',}},]},{path: '/product',component: () => import('@/layout/index.vue'),name: 'Product',meta: {title: '商品管理',icon: 'Goods',},children: [{path: '/product/trademark',component: () => import('@/views/product/trademark/index.vue'),name: 'Trademark',meta: {title: '品牌管理',icon: 'ShoppingCartFull',}},{path: '/product/attr',component: () => import('@/views/product/attr/index.vue'),name: 'Attr',meta: {title: '属性管理',icon: 'ChromeFilled',}},{path: '/product/spu',component: () => import('@/views/product/spu/index.vue'),name: 'Spu',meta: {title: 'SPU管理',icon: 'Calendar',}},{path: '/product/sku',component: () => import('@/views/product/sku/index.vue'),name: 'Sku',meta: {title: 'SKU管理',icon: 'Orange',}},]},{// 任意路由path: '/:pathMatch(.*)*',redirect: '/404',name: 'Any',meta: {title: '任意路由',hidden: true,icon: 'Wallet',}}
]

 layout右侧展示区域封装成一个组件 main(为了实现一些动画效果):src/layout/main/main.vue

关于路由过度可参考官网:过渡动效 | Vue Router (vuejs.org)

<template><!-- 路由组件出口的位置 --><router-view v-slot="{ Component }"><transition name="fade"><!-- 渲染layout一级路由组件的子路由 --><component :is="Component" /></transition></router-view>
</template><script setup lang="ts"></script><style scoped>
.fade-enter-from {opacity: 0;transform: scale(0);
}.fade-enter-active {transition: all .3s;
}.fade-enter-to {opacity: 1;transform: scale(1);
}
</style>

在layout组件中引入main:src/layout/index.vue

// 右侧内容展示组件
import Main from '@/layout/main/index.vue'
​
<!-- 内容展示区域 -->
<div class="layout_main"><Main />
</div>

 3.4 顶部tabbar组件

3.4.1 顶部tabbar组件静态搭建与拆分 

左侧菜单刷新折叠问题解决:src/layout/index.vue

// el-menu中新增default-active属性
<el-menu background-color="#001529" text-color="white" :default-active="$route.path"><!-- 根据路由动态生成菜单 --><Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>// 获取路由对象
import { useRoute } from 'vue-router'
let $route = useRoute()

 tabbar组件封装:拆分成左侧面包屑组件(breadcrumb)和右侧设置组件(setting)

面包屑组件:scr/layout/tabbar/breadcrumb/index.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></style>

设置组件:scr/layout/tabbar/setting/index.vue

<template><el-button size="small" icon="Refresh" circle></el-button><el-button size="small" icon="FullScreen" circle></el-button><el-button size="small" icon="Setting" circle></el-button><img src="/public/logo.png" style="width: 20px;height: 20px;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></style>

 tabbar组件:scr/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 scoped lang="scss">
.tabbar {width: 100%;height: 100%;display: flex;justify-content: space-between;.tabbar_left {display: flex;margin-left: 20px;align-items: center;}.tabbar_right {display: flex;align-items: center;}
}
</style>

 3.4.2 菜单折叠效果实现

定义控制折叠/展开响应式数据fold:src/store/modules/setting.ts

 因为layout组件和breadcrumb组件都需要用到fold,说定义在仓库比较合适。

// 小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'const useLayoutSettingStore = defineStore('SettingStore', {state: () => {return {fold: false, // 用户控制菜单折叠还是收起}}
})export default useLayoutSettingStore

 面包屑组件折叠图标切换实现:src/layout/tabbar/breadcrumb/index.vue

<template><!-- 顶部左侧静态 --><el-icon style="margin-right: 10px;" @click="changeIcon"><component :is="layoutSettingStore.fold ? 'Expand' : 'Fold'"></component></el-icon>......
</template><script setup lang="ts">
import useLayoutSettingStore from "@/store/modules/setting";
// 获取layout配置相关的仓库
let layoutSettingStore = useLayoutSettingStore()
// 点击图标的方法
const changeIcon = () => {// 图标进行切换layoutSettingStore.fold = !layoutSettingStore.fold
}
</script>

layout组件菜单折叠效果实现:src/layout/index.vue

步骤:

  • 通过el-menu标签的collapse属性配合fold实现菜单折叠/展开效果
  • 给左侧菜单、顶部导航、右侧内容展示区域添加动态类fold实现折叠/展开的布局改变

PS:折叠之后图标不见的问题:将icon标签放在title插槽外面

<template><div class="layout_container"><!-- 左侧菜单 --><div class="layout_slider" :class="{ fold: layoutSettingStore.fold ? true : false }"><Logo></Logo><!-- 展示菜单 --><!-- 滚动组件 --><el-scrollbar class="scrollbar"><!-- 菜单组件 --><el-menu background-color="#001529" text-color="white" :default-active="$route.path":collapse="layoutSettingStore.fold"><!-- 根据路由动态生成菜单 --><Menu :menuList="userStore.menuRoutes"></Menu></el-menu></el-scrollbar></div><!-- 顶部导航 --><div class="layout_tabbar" :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 useLayoutSettingStore from "@/store/modules/setting";
let userStore = useUserStore()
// 获取layout配置仓库
let layoutSettingStore = useLayoutSettingStore()
</script><style scoped lang="scss">
.layout_container {......&.fold {width: $base-menu-min-height;}}.layout_tabbar {......&.fold {width: calc(100vw - $base-menu-min-height);left: $base-menu-min-height;}}.layout_main {......&.fold {width: calc(100vw - $base-menu-min-height);left: $base-menu-min-height;}}
}
</style>

3.4.3 顶部面包屑动态展示 

  • 通过$route.matched获取匹配的路由信息实现动态展示。
  • 点击首页不需要展示layout路由,所以删除router.ts文件中layout路由中的元信息title和icon的值,并通过v-show判断是否展示。
  • 通过 :to 可使点击面包屑跳转匹配路由。

src/layout/tabbar/breadcrumb/index.vue

 <!-- 左侧面包屑 -->
<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>// 获取路由对象
import { useRoute } from 'vue-router'
let $route = useRoute()

 PS:点击商品管理、权限管理等一级路由的面包屑时,默认跳转到它的首个二级路由,因此需要在router.ts文件中给商品管理、权限管理的路由添加重定向。

redirect: '/acl/user',
redirect: '/product/trademark',

 3.4.4 刷新业务的实现

刷新业务就是路由组件销毁和重建的过程。涉及顶部导航组件和内容区域组件通信,因此可以使用store存储刷新业务相关标识。

小仓库中添加刷新标识数据:src/store/modules/setting.ts

refresh: false,// 用于控制刷新效果

顶部导航setting组件实现控制下仓库refresh变化 :src/layout/tabbar/setting/index.vue

// 给刷新按钮绑定点击事件
<el-button size="small" icon="Refresh" circle @click="updateRefresh"></el-button>// 获取仓库中刷新标识
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 刷新按钮点击回调
const updateRefresh = () => {// 更新刷新标识layoutSettingStore.refresh = !layoutSettingStore.refresh
}

main组件中监听小仓库refresh是否变化,控制路由销毁与重建:src/layout/main/index.vue

 <component :is="Component" v-if="flag" />import { watch, ref, nextTick } from 'vue'
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 控制当前组件是否销毁重建
let flag = ref(true)
// 监听仓库内部数据是否发生变化,如果发生变化,说明用户点击过刷新按钮
watch(() => layoutSettingStore.refresh, () => {// 点击刷新按钮:路由组件销毁flag.value = falsenextTick(() => {flag.value = true})
})

3.4.5 全屏模式的切换

这里利用DOM实现全屏切换(不同浏览器可能会有兼容问题),也可以使用插件实现。 

src/layout/tabbar/setting/index.vue 

// 给全屏按钮绑定点击事件
<el-button size="small" icon="FullScreen" circle @click="fullScreen"></el-button>// 全屏按钮点击回调
const fullScreen = () => {// DOM对象的一个属性:可以用来判断当前是不是全屏模式(全屏:true,不是全屏:false)let full = document.fullscreenElement// 切换为全屏模式if (!full) {// 文档根节点的方法requestFullscreen,实现全屏模式document.documentElement.requestFullscreen()} else {// 变为不是全屏模式 -> 退出全屏模式document.exitFullscreen()}
}

 3.4.6 获取用户信息与token理解

发生登录请求时由后端返回的唯一标识,后续向后端发送各种请求都需要携带token,因此token作为每次请求都需带的公共参数,放在请求拦截器里,通过config配置项hearders携带最合适。

src/utils/request.ts

// 引入用户相关的小仓库
import useUserStore from '@/store/modules/user';request.interceptors.request.use((config) => {// config配置对象,包括hearders属性请求头,经常给服务端携带公共参数let useStore = useUserStore()if(useStore.token){config.headers.token = useStore.token}// 返回配置对象return config;
});

home首页挂载完毕发请求获取用户信息:src/views/home/index.vue

import {onMounted} from 'vue'
// 获取仓库
import useUserStore from '@/store/modules/user';
let useStore = useUserStore()
// 目前首页挂载完毕发请求获取用户信息
onMounted(() => {useStore.userInfo()
})

 用户小仓库:src/store/modules/user.ts

在type.ts文件中定义username、avatar类型:

username: string,
avatar: string
 // 小仓库存储数据的地方state: (): UserState => {return {......username:'',avatar:''}},// 异步|逻辑的地方actions: {......// 获取用户信息async userInfo(){// 获取用户信息进行存储仓库当中(用户头像、名字)let result = await reqUserInfo()// 如果获取信息成功,存储下用户信息if(result.code === 200){this.username = result.data.checkUser.usernamethis.avatar = result.data.checkUser.avatar}}},

在setting组件中,通过user小仓库获取用户信息进行展示:src/layout/tabbar/setting/index.vue 

 ......
<img :src="useStore.avatar" style="width: 20px;height: 20px;margin: 0 10px;border-radius: 50%;"><!-- 下拉菜单 -->
<el-dropdown><span class="el-dropdown-link">{{ useStore.username }}</span>......
</el-dropdown>// 获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
let useStore = useUserStore()

3.4.7 退出登录业务

退出登录时,需要做的事情 :

  • 需要向服务器发请求(退出登录接口)
  • 仓库中关于用户相关的数据清空(token|username|avatar)
  • 跳转到登录页面

 src/layout/tabbar/setting/index.vue

<el-dropdown-item @click="logout">退出登录</el-dropdown-item>// 退出登录点击回调
const logout = () => {// 第一件事情:需要向服务器发请求(退出登录接口)----目前还没有// 第二件事情:仓库中关于用户相关的数据清空(token|username|avatar)useStore.userLogout()// 第三件事情:跳转到登录页面,通过query参数传递退出登录前的路径$router.push({ path: '/login', query: { redirect: $route.path } })
}

 封装删除token本地存储的方法:src/utils/token.ts 

// 本地存储删除数据方法
export const REMOVE_TOKEN = () => {localStorage.removeItem('TOKEN')
}

 用户小仓库:src/store/modules/user.ts

// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'// 退出登录
userLogout() {// 目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失败)this.token = ''this.username = ''this.avatar = ''REMOVE_TOKEN()}

login组件添加登录前判断跳转路由的逻辑:src/views/login/index.vue

import { useRouter, useRoute } from 'vue-router'
// 获取路由对象
let $route = useRoute()......// 判断登录的时候,路由的路径当中是否有query参数,如果有就往query参数跳转,没有就跳转到首页
let redirect: any = $route.query.redirect
$router.push({ path: redirect || '/' })

四、路由鉴权和进度条业务 

路由鉴权: 项目中能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)。

安装nprogress插件:pnpm i nprogress

src/permission.ts 

// 路由鉴权:项目中能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)
import router from '@/router'
import setting from '@/setting'
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
// @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'
let useStore = useUserStore(pinia)
// 全局守卫:项目中任意路由切换都会触发的钩子
// 全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {// to:你将要访问哪个路由// from:你从哪个路由而来// next:路由的放行函数// 进度条开始nprogress.start()// 获取token,去判断用户登录,还是未登录let token = useStore.token// 获取用户名字let username = useStore.username// 用户登录判断if (token) {// 登录成功,不能访问login,指向homeif (to.path == '/login') {next({ path: '/' })} else {// 登录成功访问其余六个路由(登录排除)// 有用户信息if (username) {// 放行next()} else {// 如果没有用户信息,在守卫这里发请求获取到了用户信息再放行try {// 获取用户信息await useStore.userInfo()// 放行next()} catch (error) {// token过期:获取不到用户信息了// 用户手动修改本地存储token// 退出登录->用户相关的数据清空useStore.userLogout()next({ path: '/login' })}}}} else {// 用户未登录判断if (to.path == '/login') {next()} else {next({ path: '/login', query: { redirect: to.path } })}}
})
// 全局后置守卫
router.afterEach((to: any, from: any) => {document.title = `${setting.title} - ${to.meta.title}`// 进度条结束nprogress.done()
})// 第一个问题:任意路由切换实现进度条业务 ---nprogress
// 第二个问题:路由鉴权(路由组件访问权限的设置)
// 全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)// 用户未登录:可以访问login,其余六个路由不能访问(指向login)
// 用户登录成功:不可以访问login(指向首页)

PS:在组件的外部通过同步的语句获取仓库的数据是拿不到的。如果想获取小仓库的数据,必须先得有大仓库(pinia) 。

在入口文件(main.ts)引入鉴权文件

// 引入路由鉴权文件
import './permission'

 五、真实接口替换mock接口和接口ts类型定义

1. 替换各个环境下的服务器地址( .env.development、.env.production、.env.test )

2. 配饰代理跨域:vite.config.ts(具体配置参数可参考官网:开发服务器选项 | 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/, ''),}}}}
})

 3. 重新书写API接口文件及接口类型文件

src/api/user/index.ts

// 统一管理项目用户相关的接口
import request from "@/utils/request";
import type { loginFormData, loginResponseData, userInfoResponeData } 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: loginFormData) => request.post<any, loginResponseData>(API.LOGIN_URL, data)
// 获取用户信息
export const reqUserInfo = () => request.get<any, userInfoResponeData>(API.USERINFO_URL)
// 退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

 src/api/user/index.ts

// 定义用户相关数据的ts类型
// 用户登录接口携带参数的ts类型
export interface loginFormData {username: string,password: string
}// 定义全部接口返回数据都拥有的ts类型
export interface ResponseData {code: number,message: string,ok: boolean
}// 定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {data: string
}// 定义获取用户信息返回的数据类型
export interface userInfoResponeData extends ResponseData {data: {routes: string[],buttons: string[],roles: string[],name: string,avatar: string}
}

 4. 修改接口相关的代码(src/store/modules/user.ts、permission.ts等文件)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/209306.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Graphpad Prism10.1.0 安装教程 (含Win/Mac版)

GraphPad Prism GraphPad Prism是一款非常专业强大的科研医学生物数据处理绘图软件&#xff0c;它可以将科学图形、综合曲线拟合&#xff08;非线性回归&#xff09;、可理解的统计数据、数据组织结合在一起&#xff0c;除了最基本的数据统计分析外&#xff0c;还能自动生成统…

Python:核心知识点整理大全8-笔记

目录 ​编辑 4.5 元组 4.5.1 定义元组 dimensions.py 4.5.2 遍历元组中的所有值 4.5.3 修改元组变量 4.6 设置代码格式 4.6.1 格式设置指南 4.6.2 缩进 4.6.3 行长 4.6.4 空行 4.6.5 其他格式设置指南 4.7 小结 第5章 if语句 5.1 一个简单示例 cars.py 5.2 条…

现代皮质沙发模型材质编辑

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 当谈到游戏角色的3D模型风格时&#xff0c;有几种不同的风格&#xf…

线性容器(QByteArray、QString、QList模板类)、堆栈窗体

QT 线性容器 点击查看&#xff1a;字符和字节的区别&#xff0c;ASCII、Unicode 和 UTF-8 编码的区别。&#xff08;&#x1f448; 安全链接&#xff0c;放心跳转&#xff09; QByteArray 思考&#xff1a;char buf[6] “hello”; 如果 C 语言中要利用 buf 内容重新生成 “…

学生备考使用台灯到底好不好?公认好用的护眼台灯推荐

在现代生活中&#xff0c;许多学生的学习压力越来越大&#xff0c;面临的近视几率也越来越大&#xff0c;特别是初中生&#xff0c;眼睛发育还未完全&#xff0c;使用不恰当的灯光也会对眼睛造成损害&#xff0c;特别是护眼台灯。虽然护眼台灯在功能上能够提供充足、柔和的光线…

《文存阅刊》期刊发表简介

《文存阅刊》以“深研文化创新&#xff0c;崇尚科学真理&#xff0c;坚持双百方针&#xff0c;打造学术精品”为办刊宗旨&#xff0c;涵盖艺术、文学、社科等多项内容&#xff0c;适应了文化市场需求&#xff0c;很好的回应了广大文化理论工作者的关切&#xff0c;为下一步打造…

ChatGPT新媒体运营神器:轻松驾驭内容创作与传播

文章目录 1. 内容创作2. 社交媒体管理3. 用户互动与客户服务 《巧用ChatGPT轻松玩转新媒体运营》内容简介作者简介目录前言/序言本书内容本书特色本书读者对象获取方式 随着互联网的高速发展&#xff0c;新媒体已经成为了人们获取信息、交流思想的重要渠道。在这个信息爆炸的时…

【SpringCache】快速入门 通俗易懂

1. 介绍 Spring Cache 是一个框架&#xff0c;实现了基于注解的缓存功能&#xff0c;只需要简单地加一个注解&#xff0c;就能实现缓存功能。 Spring Cache 提供了一层抽象&#xff0c;底层可以切换不同的缓存实现&#xff0c;例如&#xff1a; EHCache Caffeine Redis(常用…

Centos7、Mysql8.0 load_file函数返回为空的终极解决方法--暨selinux的深入理解

零、问题背景 最近想换房&#xff0c;为了方便自己对比感兴趣的房子&#xff0c;因此决定将目标房源的基本信息放在表里&#xff0c;特别是要一目了然的看到众多房子的各种图纸和照片&#xff0c;因此决定要在Mysql8.0.34数据库中以二进制形式保存图片&#xff08;抛开合理性和…

Vue 2.0源码分析-update

Vue 的 _update 是实例的一个私有方法&#xff0c;它被调用的时机有 2 个&#xff0c;一个是首次渲染&#xff0c;一个是数据更新的时候&#xff1b;由于我们这一章节只分析首次渲染部分&#xff0c;数据更新部分会在之后分析响应式原理的时候涉及。_update 方法的作用是把 VNo…

思维链(CoT)提出者 Jason Wei:关于大语言模型的六个直觉

文章目录 一、前言二、主要内容三、总结 &#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、前言 Jason Wei 的主页&#xff1a;https://www.jasonwei.net/ Jason Wei&#xff0c;一位于 2020 年从达特茅斯学院毕业的杰出青年&#xff0c;随后加盟了…

大数据安全保障的四种关键技术

随着大数据时代的到来&#xff0c;数据安全保障的重要性日益凸显。大数据安全保障涉及多种关键技术&#xff0c;以下是四种关键技术的详细介绍。 数据加密技术 数据加密技术是大数据安全保障的核心技术之一。它通过将明文数据转化为密文数据&#xff0c;以保护数据的机密性和完…

CSS中 设置文字下划线 的几种方法

在网页设计和开发中&#xff0c;我们经常需要对文字进行样式设置&#xff0c;包括字体,颜色&#xff0c;大小等&#xff0c;其中&#xff0c;设置文字下划线是一种常见需求 一 、CSS种使用 text-decoration 属性来设置文字的装饰效果&#xff0c;包括下划线。 常用的取值&…

炫酷不止一面:探索JavaScript动画的奇妙世界(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

proftpd安全加固:限制用户FTP登录

其实无所谓安全加固&#xff0c;因为proftp默认就是限制用户FTP登录的&#xff0c;这里有点凌乱得研究和实验了proftpd如何进行限制的&#xff0c;以及可能的放开限制。懂了这些才能更好的进行防护配置。 RootLogin指令其实主要作用就是启用ROOT访问。通常&#xff0c;proftpd在…

【Fastadmin】一个完整的轮播图功能示例

目录 1.效果展示&#xff1a; 列表 添加及编辑页面同 2.建表&#xff1a; 3.使用crud一键生成并创建控制器 4.html页面 add.html edit.html index.php 5.js页面 6.小知识点 1.效果展示&#xff1a; 列表 添加及编辑页面同 2.建表&#xff1a; 表名&#xff1a;fa_x…

【LabVIEW学习】5.数据通信之TCP协议,控制电脑的一种方式

一。tcp连接以及写数据&#xff08;登录&#xff09; 数据通信--》协议--》TCP 1.tcp连接 创建while循环&#xff0c;中间加入事件结构&#xff0c;创建tcp连接&#xff0c;写入IP地址与端口号 2.写入tcp数据 登录服务器除了要知道IP地址以及端口以外&#xff0c;需要用户名与密…

中通单号查询,中通快递物流查,备注需要的单号记录

批量查询中通快递单号的物流信息&#xff0c;并对需要的单号记录进行备注。 所需工具&#xff1a; 一个【快递批量查询高手】软件 中通快递单号若干 操作步骤&#xff1a; 步骤1&#xff1a;运行【快递批量查询高手】软件&#xff0c;第一次使用的朋友记得先注册&#xff0c…

快速幂(C语言)

前言 快速幂算法一般用于高次幂取模的题目中&#xff0c;比如求3的10000次方对7取模。这时候有些同学会说&#xff1a;这还不简单&#xff1f;我直接调用pow函数然后对结果%7不得了么&#xff1f;可是3的10000次方这么庞大的数字&#xff0c;真的能储存在计算机里么&#xff1f…

c#学习相关系列之as和is的相关用法

一、子类和父类的关系 public class Program{static void Main(string[] args){Animal animal new Dog();// Dog dog (Dog)new Animal(); 编译成功&#xff0c;运行报错Dog dog (Dog)animal;Dog dog new Dog();Animal animal dog; //等价于Animal animal new Dog();}}pub…