1.路由配置
1.1路由组件的雏形
src\views\home\index.vue
(以home组件为例)
1.2路由配置
1.2.1路由index文件
src\router\index.ts
//通过vue-router插件实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoute } from './router'
//创建路由器
const router = createRouter({
//路由模式hash
history: createWebHashHistory(),
routes: constantRoute,
//滚动行为
scrollBehavior() {
return {
left: 0,
top: 0,
}
},
})
export default router
1.2.2路由配置
src\router\router.ts
//对外暴露配置路由(常量路由)
export const constantRoute = [
{
//登录路由
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login', //命名路由
},
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/views/home/index.vue'),
name: 'layout',
},
{
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
},
{
//重定向
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
},
]
1.3路由出口
src\App.vue
2.登录模块
2.1 登录路由静态组件
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_form">
<h1>Hello</h1>
<h2>欢迎来到硅谷甄选</h2>
<el-form-item>
<el-input
:prefix-icon="User"
v-model="loginForm.username"
></el-input>
</el-form-item>
<el-form-item>
<el-input
type="password"
:prefix-icon="Lock"
v-model="loginForm.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 loginForm = reactive({ username: 'admin', password: '111111' })
</script>
<style lang="scss" scoped>
.login_container {
width: 100%;
height: 100vh;
background: url('@/assets/images/background.jpg') no-repeat;
background-size: cover;
.login_form {
position: relative;
width: 80%;
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 0px;
}
.login_btn {
width: 100%;
}
}
}
</style>
注意:el-col是24份的,在此左右分为了12份。我们在右边放置我们的结构。:xs="0"
是为了响应式。el-form
下的element-plus元素都用el-form-item
包裹起来。
2.2 登陆业务实现
2.2.1 登录按钮绑定回调
回调应该做的事情
const login = () => {
//点击登录按钮以后干什么
//通知仓库发起请求
//请求成功->路由跳转
//请求失败->弹出登陆失败信息
}
2.2.2 仓库store初始化
- 大仓库(笔记只写一次)
安装pinia:pnpm i pinia@2.0.34
src\store\index.ts
//仓库大仓库
import { createPinia } from 'pinia'
//创建大仓库
const pinia = createPinia()
//对外暴露:入口文件需要安装仓库
export default pinia
- 用户相关的小仓库
src\store\modules\user.ts
//创建用户相关的小仓库
import { defineStore } from 'pinia'
//创建用户小仓库
const useUserStore = defineStore('User', {
//小仓库存储数据地方
state: () => {},
//处理异步|逻辑地方
actions: {},
getters: {},
})
//对外暴露小仓库
export default useUserStore
2.2.3 按钮回调
//登录按钮的回调
const login = async () => {
//按钮加载效果
loading.value = true
//点击登录按钮以后干什么
//通知仓库发起请求
//请求成功->路由跳转
//请求失败->弹出登陆失败信息
try {
//也可以书写.then语法
await useStore.userLogin(loginForm)
//编程式导航跳转到展示数据的首页
$router.push('/')
//登录成功的提示信息
ElNotification({
type: 'success',
message: '登录成功!',
})
//登录成功,加载效果也消失
loading.value = false
} catch (error) {
//登陆失败加载效果消失
loading.value = false
//登录失败的提示信息
ElNotification({
type: 'error',
message: (error as Error).message,
})
}
}
2.2.4 用户仓库
//创建用户相关的小仓库
import { defineStore } from 'pinia'
//引入接口
import { reqLogin } from '@/api/user'
//引入数据类型
import type { loginForm } from '@/api/user/type'
//创建用户小仓库
const useUserStore = defineStore('User', {
//小仓库存储数据地方
state: () => {
return {
token: localStorage.getItem('TOKEN'), //用户唯一标识token
}
},
//处理异步|逻辑地方
actions: {
//用户登录的方法
async userLogin(data: loginForm) {
//登录请求
const result: any = await reqLogin(data)
if (result.code == 200) {
//pinia仓库存储token
//由于pinia|vuex存储数据其实利用js对象
this.token = result.data.token
//本地存储持久化存储一份
localStorage.setItem('TOKEN', result.data.token)
//保证当前async函数返回一个成功的promise函数
return 'ok'
} else {
return Promise.reject(new Error(result.data.message))
}
},
},
getters: {},
})
//对外暴露小仓库
export default useUserStore
2.2.5 小结
- Element-plus中ElNotification用法(弹窗):
引入:import { ElNotification } from 'element-plus'
使用:
//登录失败的提示信息
ElNotification({
type: 'error',
message: (error as Error).message,
})
- Element-plus中el-button的loading属性。
- pinia使用actions、state的方式和vuex不同:需要引入函数和创建实例
- $router的使用:也需要引入函数和创建实例
- 在actions中使用state的token数据:this.token
- 类型定义需要注意。
- promise的使用和vue2现在看来是一样的。
2.3模板封装登陆业务
2.3.1 result返回类型封装
interface dataType {
token?: string
message?: string
}
//登录接口返回的数据类型
export interface loginResponseData {
code: number
data: dataType
}
2.3.2 State仓库类型封装
//定义小仓库数据state类型
export interface UserState {
token: string | null
}
2.3.3 本地存储封装
将本地存储的方法封装到一起
//封装本地存储存储数据与读取数据方法
export const SET_TOKEN = (token: string) => {
localStorage.setItem('TOKEN', token)
}
export const GET_TOKEN = () => {
return localStorage.getItem('TOKEN')
}
2.4 登录时间的判断
- 封装函数
//封装函数:获取当前时间段
export const getTime = () => {
let message = ''
//通过内置构造函数Date
const hour = new Date().getHours()
if (hour <= 9) {
message = '早上'
} else if (hour <= 14) {
message = '上午'
} else if (hour <= 18) {
message = '下午'
} else {
message = '晚上'
}
return message
}
- 使用(引入后)
- 效果
2.5 表单校验规则
2.5.1 表单校验
- 表单绑定项
:model:绑定的数据
//收集账号与密码数据
let loginForm = reactive({ username: 'admin', password: '111111' })
:rules:对应要使用的规则
//定义表单校验需要的配置对象
const rules = {}
ref="loginForms":获取表单元素
//获取表单元素
let loginForms = ref()
- 表单元素绑定项
Form 组件提供了表单验证的功能,只需为 rules 属性传入约定的验证规则,并将 form-Item 的 prop 属性设置为需要验证的特殊键值即可
- 使用规则rules
//定义表单校验需要的配置对象
const rules = {
username: [
//规则对象属性:
{
required: true, // required,代表这个字段务必要校验的
min: 5, //min:文本长度至少多少位
max: 10, // max:文本长度最多多少位
message: '长度应为6-10位', // message:错误的提示信息
trigger: 'change', //trigger:触发校验表单的时机 change->文本发生变化触发校验, blur:失去焦点的时候触发校验规则
},
],
password: [
{
required: true,
min: 6,
max: 10,
message: '长度应为6-15位',
trigger: 'change',
},
],
}
- 校验规则通过后运行
const login = async () => {
//保证全部表单项校验通过
await loginForms.value.validate()
。。。。。。
}
2.5.2自定义表单校验
- 修改使用规则rules
使用自己编写的函数作为规则校验。
//定义表单校验需要的配置对象
const rules = {
username: [
//规则对象属性:
/* {
required: true, // required,代表这个字段务必要校验的
min: 5, //min:文本长度至少多少位
max: 10, // max:文本长度最多多少位
message: '长度应为6-10位', // message:错误的提示信息
trigger: 'change', //trigger:触发校验表单的时机 change->文本发生变化触发校验, blur:失去焦点的时候触发校验规则
}, */
{ trigger: 'change', validator: validatorUserName },
],
password: [
{ trigger: 'change', validator: validatorPassword },
],
}
- 自定义校验规则函数
//自定义校验规则函数
const validatorUserName = (rule: any, value: any, callback: any) => {
//rule:校验规则对象
//value:表单元素文本内容
//callback:符合条件,callback放行通过,不符合:注入错误提示信息
if (value.length >= 5) {
callback()
} else {
callback(new Error('账号长度至少5位'))
}
}
const validatorPassword = (rule: any, value: any, callback: any) => {
if (value.length >= 6) {
callback()
} else {
callback(new Error('密码长度至少6位'))
}
}
3. Layout模块(主界面)
3.1 组件的静态页面
3.1.1 组件的静态页面
注意:我们将主界面单独放一个文件夹(顶替原来的home路由组件)。注意修改一下路由配置
<template>
<div class="layout_container">
<!-- 左侧菜单 -->
<div class="layout_slider"></div>
<!-- 顶部导航 -->
<div class="layout_tabbar"></div>
<!-- 内容展示区域 -->
<div class="layout_main">
<p style="height: 1000000px"></p>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.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: cyan;
top: 0;
left: $base-menu-width;
}
.layout_main {
position: absolute;
width: calc(100% - $base-menu-width);
height: calc(100vh - $base-tabbar-height);
background-color: yellowgreen;
left: $base-menu-width;
top: $base-tabbar-height;
padding: 20px;
overflow: auto;
}
}
</style>
3.1.2定义部分全局变量&滚动条
scss全局变量
//左侧菜单宽度
$base-menu-width :260px;
//左侧菜单背景颜色
$base-menu-background: #001529;
//顶部导航的高度
$base-tabbar-height:50px;
滚动条
//滚动条外观设置
::-webkit-scrollbar{
width: 10px;
}
::-webkit-scrollbar-track{
background: $base-menu-background;
}
::-webkit-scrollbar-thumb{
width: 10px;
background-color: yellowgreen;
border-radius: 10px;
}
3.2 Logo子组件的搭建
页面左上角的这部分,我们将它做成子组件,并且封装方便维护以及修改。
3.2.1 Logo子组件
在这里我们引用了封装好的setting
<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 lang="scss" scoped>
.logo {
width: 100%;
height: $base-menu-logo-height;
color: white;
display: flex;
align-items: center;
padding: 20px;
img {
width: 40px;
height: 40px;
}
p {
font-size: $base-logo-title-fontSize;
margin-left: 10px;
}
}
</style>
3.2.2 封装setting
为了方便我们以后对logo以及标题的修改。
//用于项目logo|标题配置
export default {
title: '硅谷甄选运营平台', //项目的标题
logo: '/public/logo.png', //项目logo设置
logoHidden: true, //logo组件是否隐藏
}
3.2.3 使用
在layout组件中引入并使用
3.3 左侧菜单组件
3.3.1静态页面(未封装)
主要使用到了element-plus的menu组件。附带使用了滚动组件
<!-- 左侧菜单 -->
<div class="layout_slider">
<Logo></Logo>
<!-- 展示菜单 -->
<!-- 滚动组件 -->
<el-scrollbar class="scrollbar">
<!-- 菜单组件 -->
<el-menu background-color="#001529" text-color="white">
<el-menu-item index="1">首页</el-menu-item>
<el-menu-item index="2">数据大屏</el-menu-item>
<!-- 折叠菜单 -->
<el-sub-menu index="3">
<template #title>
<span>权限管理</span>
</template>
<el-menu-item index="3-1">用户管理</el-menu-item>
<el-menu-item index="3-2">角色管理</el-menu-item>
<el-menu-item index="3-3">菜单管理</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</div>
3.3.2 递归组件生成动态菜单
在这一部分,我们要根据路由生成左侧的菜单栏
- 将父组件中写好的子组件结构提取出去
<!-- 展示菜单 -->
<!-- 滚动组件 -->
<el-scrollbar class="scrollbar">
<!-- 菜单组件 -->
<el-menu background-color="#001529" text-color="white">
<!-- 更具路由动态生成菜单 -->
<Menu></Menu>
</el-menu>
</el-scrollbar>
- 动态菜单子组件:src\layout\menu\index.vue
- 处理路由
因为我们要根据路由以及其子路由作为我们菜单的一级|二级标题。因此我们要获取路由信息。
给路由中加入了路由元信息meta:它包含了2个属性:title以及hidden
{
//登录路由
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login', //命名路由
meta: {
title: '登录', //菜单标题
hidden: true, //路由的标题在菜单中是否隐藏
},
},
- 仓库引入路由并对路由信息类型声明(vue-router有对应函数)
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
。。。。。
//小仓库存储数据地方
state: (): UserState => {
return {
token: GET_TOKEN(), //用户唯一标识token
menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)
}
- 父组件拿到仓库路由信息并传递给子组件
<script setup lang="ts">
。。。。。。
//引入菜单组件
import Menu from './menu/index.vue'
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
</script>
- 子组件prps接收并且处理结构
<template>
<template v-for="(item, index) in menuList" :key="item.path">
<!-- 没有子路由 -->
<template v-if="!item.children">
<el-menu-item v-if="!item.meta.hidden" :index="item.path">
<template #title>
<span>标</span>
<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"
>
<template #title>
<span>标</span>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且个数大于一个 -->
<el-sub-menu
:index="item.path"
v-if="item.children && item.children.length >= 2"
>
<template #title>
<span>{{ item.meta.title }}</span>
</template>
<Menu :menuList="item.children"></Menu>
</el-sub-menu>
</template>
</template>
<script setup lang="ts">
//获取父组件传递过来的全部路由数组
defineProps(['menuList'])
</script>
<script lang="ts">
export default {
name: 'Menu',
}
</script>
<style lang="scss" scoped></style>
注意: 1:因为每一个项我们要判断俩次(是否要隐藏,以及子组件个数),所以在el-menu-item外面又套了一层模板 2:当子路由个数大于等于一个时,并且或许子路由还有后代路由时。这里我们使用了递归组件。递归组件需要命名(另外使用一个script标签,vue2格式)。
3.3.3 菜单图标
- 注册图标组件
因为我们要根据路由配置对应的图标,也要为了后续方便更改。因此我们将所有的图标注册为全局组件。(使用之前将分页器以及矢量图注册全局组件的自定义插件)(所有图标全局注册的方法element-plus文档中已给出)
。。。。。。
//引入element-plus提供全部图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
。。。。。。
//对外暴露插件对象
export default {
//必须叫做install方法
//会接收我们的app
。。。。。。
//将element-plus提供全部图标注册为全局组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
},
}
- 给路由元信息添加属性:icon
以laytou和其子组件为例:首先在element-puls找到你要使用的图标的名字。将它添加到路由元信息的icon属性上
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: 'layout',
hidden: false,
icon: 'Avatar',
},
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
hidden: false,
icon: 'HomeFilled',
},
},
],
},
- 菜单组件使用
以只有一个子路由的组件为例:
<!-- 有且只有一个子路由 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item
index="item.children[0].path"
v-if="!item.children[0].meta.hidden"
>
<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>
3.3.4 项目全部路由配置
- 全部路由配置(以权限管理为例)
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
hidden: false,
title: '权限管理',
icon: 'Lock',
},
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
hidden: false,
title: '用户管理',
icon: 'User',
},
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
hidden: false,
title: '角色管理',
icon: 'UserFilled',
},
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
hidden: false,
title: '菜单管理',
icon: 'Monitor',
},
},
],
},
- 添加路由跳转函数
第三种情况我们使用组件递归,所以只需要给前面的2个添加函数
<script setup lang="ts">
。。。。。。
//获取路由器对象
let $router = useRouter()
const goRoute = (vc: any) => {
//路由跳转
$router.push(vc.index)
}
</script>
- layout组件
3.3.5 Bug&&总结
在这部分对router-link遇到一些bug,理解也更深了,特意写一个小结总结一下
bug:router-link不生效。 描述:当我点击跳转函数的时候,直接跳转到一个新页面,而不是layout组件展示的部分更新。 思路:首先输出了一下路径,发现路径没有错。其次,因为跳转到新页面,代表layout组件中的router-link不生效,删除router-link,发现没有影响。所以确定了是router-link没有生效。 解决:仔细检查了src\router\routes.ts
文件,最后发现一级路由的component关键字写错。导致下面的二级路由没有和以及路由构成父子关系。所以会跳转到APP组件下的router-link 总结:router-link会根据下面的子路由来进行展示。如果发生了路由跳转不对的情况,去仔细检查一下路由关系有没有写对。APP是所有一级路由组件的父组件
3.3.6 动画 && 自动展示
- 将router-link封装成单独的文件并且添加一些动画
<template>
<!-- 路由组件出口的位置 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染layout一级路由的子路由 -->
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.fade-enter-from {
opacity: 0;
}
.fade-enter-active {
transition: all 0.3s;
}
.fade-enter-to {
opacity: 1;
}
</style>
- 自动展示
当页面刷新时,菜单会自动收起。我们使用element-plus的*default-active *处理。$router.path为当前路由。 src\layout\index.vue
3.4 顶部tabbar组件
3.4.1静态页面
element-plus:breadcrumb el-button el-dropdown
<template>
<div class="tabbar">
<div class="tabbar_left">
<!-- 顶部左侧的图标 -->
<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>
</div>
<div class="tabbar_right">
<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: 24px; height: 24px; margin: 0px 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>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
background-image: linear-gradient(
to right,
rgb(236, 229, 229),
rgb(151, 136, 136),
rgb(240, 234, 234)
);
.tabbar_left {
display: flex;
align-items: center;
margin-left: 20px;
}
.tabbar_right {
display: flex;
align-items: center;
}
}
</style>
组件拆分:
<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 lang="scss" scoped></style>
<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: 24px; height: 24px; margin: 0px 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 lang="scss" scoped></style>
3.4.2 菜单折叠
- 折叠变量
定义一个折叠变量来判断现在的状态是否折叠。因为这个变量同时给breadcrumb组件以及父组件layout使用,因此将这个变量定义在pinia中
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //用户控制菜单折叠还是收起的控制
}
},
})
export default useLayOutSettingStore
- 面包屑组件点击图标切换状态
<template>
<!-- 顶部左侧的图标 -->
<el-icon style="margin-right: 10px" @click="changeIcon">
<component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></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组件根据fold状态来修改个子组件的样式(以左侧菜单为例)
绑定动态样式修改scss
- 左侧菜单使用element-plus折叠collapse属性
效果图: 注意:折叠文字的时候会把图标也折叠起来。在menu组件中吧图标放到template外面就可以。
3.4.3 顶部面包屑动态展示
- 引入$route
注意$router和$route是不一样的
<script setup lang="ts">
import { useRoute } from 'vue-router'
//获取路由对象
let $route = useRoute()
//点击图标的切换
</script>
- 结构展示
注意:使用了$route.matched函数,此函数能得到当前路由的信息
- 首页修改
访问首页时,因为它是二级路由,会遍历出layout面包屑,处理:删除layout路由的title。再加上一个判断
- 面包屑点击跳转
注意:将路由中的一级路由权限管理以及商品管理重定向到第一个孩子,这样点击跳转的时候会定向到第一个孩子。
3.4.4 刷新业务的实现
- 使用pinia定义一个变量作为标记
- 点击刷新按钮,修改标记
<script setup lang="ts">
//使用layout的小仓库
import useLayOutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayOutSettingStore()
//刷新按钮点击的回调
const updateRefresh = () => {
layoutSettingStore.refresh = !layoutSettingStore.refresh
}
</script>
- main组件检测标记销毁&重加载组件(nextTick)
<script setup lang="ts">
import { watch, ref, nextTick } from 'vue'
//使用layout的小仓库
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
//控制当前组件是否销毁重建
let flag = ref(true)
//监听仓库内部的数据是否发生变化,如果发生变化,说明用户点击过刷新按钮
watch(
() => layOutSettingStore.refresh,
() => {
//点击刷新按钮:路由组件销毁
flag.value = false
nextTick(() => {
flag.value = true
})
},
)
</script>
3.4.5 全屏模式的实现
- 给全屏按钮绑定函数
- 实现全屏效果(利用docment根节点的方法)
//全屏按钮点击的回调
const fullScreen = () => {
//DOM对象的一个属性:可以用来判断当前是不是全屏的模式【全屏:true,不是全屏:false】
let full = document.fullscreenElement
//切换成全屏
if (!full) {
//文档根节点的方法requestFullscreen实现全屏
document.documentElement.requestFullscreen()
} else {
//退出全屏
document.exitFullscreen()
}
4.部分功能处理完善
==登录这一块大概逻辑,前端发送用户名密码到后端,后端返回token,前端保存,并且请求拦截器,请求头有token就要携带token==
4.1 登录获取用户信息(TOKEN)
登录之后页面(home)上来就要获取用户信息。并且将它使用到页面中
- home组件挂载获取用户信息
<script setup lang="ts">
//引入组合是API生命周期函数
import { onMounted } from 'vue'
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
onMounted(() => {
userStore.userInfo()
})
</script>
- 小仓库中定义用户信息以及type声明
import type { RouteRecordRaw } from 'vue-router'
//定义小仓库数据state类型
export interface UserState {
token: string | null
menuRoutes: RouteRecordRaw[]
username: string
avatar: string
}
- 请求头添加TOKEN
//引入用户相关的仓库
import useUserStore from '@/store/modules/user'
。。。。。。
//请求拦截器
request.interceptors.request.use((config) => {
//获取用户相关的小仓库,获取token,登录成功以后携带个i服务器
const userStore = useUserStore()
if (userStore.token) {
config.headers.token = userStore.token
}
//config配置对象,headers请求头,经常给服务器端携带公共参数
//返回配置对象
return config
})
- 小仓库发请求并且拿到用户信息
//获取用户信息方法
async userInfo() {
//获取用户信息进行存储
let result = await reqUserInfo()
if (result.code == 200) {
this.username = result.data.checkUser.username
this.avatar = result.data.checkUser.avatar
}
},
- 更新tabbar的信息(记得先引入并创建实例)
src\layout\tabbar\setting\index.vue
4.2 退出功能
- 退出登录绑定函数,调用仓库函数
//退出登陆点击的回调
const logout = () => {
//第一件事:需要项服务器发请求【退出登录接口】(我们这里没有)
//第二件事:仓库当中和关于用户的相关的数据清空
userStore.userLogout()
//第三件事:跳转到登陆页面
}
- pinia仓库
//退出登录
userLogout() {
//当前没有mock接口(不做):服务器数据token失效
//本地数据清空
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
},
- 退出登录,路由跳转
注意:携带的query参数方便下次登陆时直接跳转到当时推出的界面 个人觉得这个功能没什么作用。但是可以学习方法
//退出登陆点击的回调
const logout = () => {
//第一件事:需要项服务器发请求【退出登录接口】(我们这里没有)
//第二件事:仓库当中和关于用户的相关的数据清空
userStore.userLogout()
//第三件事:跳转到登陆页面
$router.push({ path: '/login', query: { redirect: $route.path } })
}
- 登录按钮进行判断
4.3 路由守卫
src\permisstion.ts
(新建文件) main.ts引入
4.3.1 进度条
- 安装
pnpm i nprogress
- 引入并使用
//路由鉴权:鉴权:项目当中路由能不能被访问的权限
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
4.3.2 路由鉴权
//路由鉴权:鉴权:项目当中路由能不能被访问的权限
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。
- 全局路由守卫将获取用户信息的请求放在了跳转之前。实现了刷新后用户信息丢失的功能。
4.4 真实接口替代mock接口
接口文档: http://139.198.104.58:8209/swagger-ui.html http://139.198.104.58:8212/swagger-ui.html#/
- 修改服务器域名
将.env.development,.env.production .env.test,三个环境文件下的服务器域名写为:
- 代理跨域
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/, ''),
},
},
},
}
})
- 修改api
在这里退出登录有了自己的api
//统一管理项目用户相关的接口
import request from '@/utils/request'
//项目用户相关的请求地址
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout',
}
//对外暴露请求函数
//登录接口方法
export const reqLogin = (data: any) => {
return request.post<any, any>(API.LOGIN_URL, data)
}
//获取用户信息接口方法
export const reqUserInfo = () => {
return request.get<any, any>(API.USERINFO_URL)
}
//退出登录
export const reqLogout = () => {
return request.post<any, any>(API.LOGOUT_URL)
}
- 小仓库(user)
替换原有的请求接口函数,以及修改退出登录函数。以及之前引入的类型显示我们展示都设置为any
//创建用户相关的小仓库
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
- 退出登录按钮的点击函数修改
退出成功后再跳转
- 路由跳转判断条件修改
src\permisstion.ts
也是退出成功后再跳转
4.5 接口类型定义
//登录接口需要携带参数类型
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文件中对发请求时的参数以及返回的数据添加类型定义
5.品牌管理模块
5.1 静态组件
使用element-plus。
<template>
<el-card class="box-card">
<!-- 卡片顶部添加品牌按钮 -->
<el-button type="primary" size="default" icon="Plus">添加品牌</el-button>
<!-- 表格组件,用于展示已有的数据 -->
<!--
table
---border:是否有纵向的边框
table-column
---lable:某一个列表
---width:设置这一列的宽度
---align:设置这一列对齐方式
-->
<el-table style="margin: 10px 0px" border>
<el-table-column
label="序号"
width="80px"
align="center"
></el-table-column>
<el-table-column label="品牌名称"></el-table-column>
<el-table-column label="品牌LOGO"></el-table-column>
<el-table-column label="品牌操作"></el-table-column>
</el-table>
<!-- 分页器组件 -->
<!--
pagination
---v-model:current-page:设置当前分页器页码
---v-model:page-size:设置每一也展示数据条数
---page-sizes:每页显示个数选择器的选项设置
---background:背景颜色
---layout:分页器6个子组件布局的调整 "->"把后面的子组件顶到右侧
-->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="limit"
:page-sizes="[3, 5, 7, 9]"
:background="true"
layout=" prev, pager, next, jumper,->,total, sizes,"
:total="400"
/>
</el-card>
</template>
<script setup lang="ts">
//引入组合式API函数
import { ref } from 'vue'
//当前页码
let pageNo = ref<number>(1)
//每一页展示的数据
let limit = ref<number>(3)
</script>
<style lang="scss" scoped></style>
5.2 数据模块
5.2.1 API
- api函数
//书写品牌管理模块接口
import request from '@/utils/request'
//品牌管理模块接口地址
enum API {
//获取已有品牌接口
TRADEMARK_URL = '/admin/product/baseTrademark/',
}
//获取一样偶品牌的接口方法
//page:获取第几页 ---默认第一页
//limit:获取几个已有品牌的数据
export const reqHasTrademark = (page: number, limit: number) =>
request.get<any, any>(API.TRADEMARK_URL + `${page}/${limit}`)
- 获取数据
我们获取数据没有放在pinia中,二是放在组件中挂载时获取数据
<script setup lang="ts">
import { reqHasTrademark } from '@/api/product/trademark'
//引入组合式API函数
import { ref, onMounted } from 'vue'
//当前页码
let pageNo = ref<number>(1)
//每一页展示的数据
let limit = ref<number>(3)
//存储已有品牌数据总数
let total = ref<number>(0)
//存储已有品牌的数据
let trademarkArr = ref<any>([])
//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可
const getHasTrademark = async (pager = 1) => {
//当前页码
pageNo.value = pager
let result = await reqHasTrademark(pageNo.value, limit.value)
console.log(result)
if (result.code == 200) {
//存储已有品牌总个数
total.value = result.data.total
trademarkArr.value = result.data.records
console.log(trademarkArr)
}
}
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
getHasTrademark()
})
</script>
5.2.2 数据展示
在数据展示模块,我们使用了element-plus的el-table,下面组要讲解属性和注意点。
- data属性:显示的数据
比如我们这里绑定的trademarkArr是个三个对象的数组,就会多出来3行。
- el-table-column的type属性:对应列的类型。 如果设置了selection则显示多选框; 如果设置了 index 则显示该行的索引(从 1 开始计算); 如果设置了 expand 则显示为一个可展开的按钮
- el-table-column的prop属性:字段名称 对应列内容的字段名, 也可以使用 property属性
注意:因为我们之前已经绑定了数据,所以在这里直接使用数据的属性tmName