# vue3_admin_templateBJ1
**Repository Path**: leo2030/vue3_admin_template-bj1
## Basic Information
- **Project Name**: vue3_admin_templateBJ1
- **Description**: 次仓库为跟随贾程豪学习vue3后台管理系统
- **Primary Language**: Unknown
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2024-02-01
- **Last Updated**: 2024-02-01
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 硅谷甄选运营平台
此次教学课程为硅谷甄选运营平台项目,包含运营平台项目模板从0到1开发,以及数据大屏幕、权限等业务。
此次教学课程涉及到技术栈包含**_:vue3+TypeScript+vue-router+pinia+element-plus+axios+echarts_**等技术栈。
## 一、vue3组件通信方式
**通信仓库地址:https://gitee.com/jch1011/vue3_communication.git**
不管是vue2还是vue3,组件通信方式很重要,不管是项目还是面试都是经常用到的知识点。
**比如:vue2组件通信方式**
**props:**可以实现父子组件、子父组件、甚至兄弟组件通信
**自定义事件**:可以实现子父组件通信
**全局事件总线$bus**:可以实现任意组件通信
**pubsub:**发布订阅模式实现任意组件通信
**vuex**:集中式状态管理容器,实现任意组件通信
**ref**:父组件获取子组件实例VC,获取子组件的响应式数据以及方法
**slot:**插槽(默认插槽、具名插槽、作用域插槽)实现父子组件通信........
### 1.1props
props可以实现父子组件通信,在vue3中我们可以通过defineProps获取父组件传递的数据。且在组件内部不需要引入defineProps方法可以直接使用!
**父组件给子组件传递数据**
```
```
**子组件获取父组件传递数据:方式1**
```
let props = defineProps({
info:{
type:String,//接受的数据类型
default:'默认参数',//接受默认数据
},
money:{
type:Number,
default:0
}})
```
**子组件获取父组件传递数据:方式2**
```
let props = defineProps(["info",'money']);
```
子组件获取到props数据就可以在模板中使用了,但是切记props是只读的(只能读取,不能修改)
### 1.2自定义事件
在vue框架中事件分为两种:一种是原生的DOM事件,另外一种自定义事件。
原生DOM事件可以让用户与网页进行交互,比如click、dbclick、change、mouseenter、mouseleave....
自定义事件可以实现子组件给父组件传递数据
#### 1.2.1原生DOM事件
代码如下:
```
我是祖国的老花骨朵
```
当前代码级给pre标签绑定原生DOM事件点击事件,默认会给事件回调注入event事件对象。当然点击事件想注入多个参数可以按照下图操作。但是切记注入的事件对象务必叫做$event.
```
我要传递多个参数
```
在vue3框架click、dbclick、change(这类原生DOM事件),不管是在标签、自定义标签上(组件标签)都是原生DOM事件。
****
#### 1.2.2自定义事件
自定义事件可以实现子组件给父组件传递数据.在项目中是比较常用的。
比如在父组件内部给子组件(Event2)绑定一个自定义事件
```
```
在Event2子组件内部触发这个自定义事件
```
我是子组件2
```
我们会发现在script标签内部,使用了defineEmits方法,此方法是vue3提供的方法,不需要引入直接使用。defineEmits方法执行,传递一个数组,数组元素即为将来组件需要触发的自定义事件类型,此方执行会返回一个$emit方法用于触发自定义事件。
当点击按钮的时候,事件回调内部调用$emit方法去触发自定义事件,第一个参数为触发事件类型,第二个、三个、N个参数即为传递给父组件的数据。
需要注意的是:代码如下
```
```
正常说组件标签书写@click应该为原生DOM事件,但是如果子组件内部通过defineEmits定义就变为自定义事件了
```
let $emit = defineEmits(["xxx",'click']);
```
### 1.3全局事件总线
全局事件总线可以实现任意组件通信,在vue2中可以根据VM与VC关系推出全局事件总线。
但是在vue3中没有Vue构造函数,也就没有Vue.prototype.以及组合式API写法没有this,
那么在Vue3想实现全局事件的总线功能就有点不现实啦,如果想在Vue3中使用全局事件总线功能
可以使用插件mitt实现。
**mitt:官网地址:https://www.npmjs.com/package/mitt**
### 1.4v-model
v-model指令可是收集表单数据(数据双向绑定),除此之外它也可以实现父子组件数据同步。
而v-model实指利用props[modelValue]与自定义事件[update:modelValue]实现的。
下方代码:相当于给组件Child传递一个props(modelValue)与绑定一个自定义事件update:modelValue
实现父子组件数据同步
```
```
在vue3中一个组件可以通过使用多个v-model,让父子组件多个数据同步,下方代码相当于给组件Child传递两个props分别是pageNo与pageSize,以及绑定两个自定义事件update:pageNo与update:pageSize实现父子数据同步
```
```
### 1.5useAttrs
在Vue3中可以利用useAttrs方法获取组件的属性与事件(包含:原生DOM事件或者自定义事件),次函数功能类似于Vue2框架中$attrs属性与$listeners方法。
比如:在父组件内部使用一个子组件my-button
```
```
子组件内部可以通过useAttrs方法获取组件属性与事件.因此你也发现了,它类似于props,可以接受父组件传递过来的属性与属性值。需要注意如果defineProps接受了某一个属性,useAttrs方法返回的对象身上就没有相应属性与属性值。
```
```
### 1.6ref与$parent
ref,提及到ref可能会想到它可以获取元素的DOM或者获取子组件实例的VC。既然可以在父组件内部通过ref获取子组件实例VC,那么子组件内部的方法与响应式数据父组件可以使用的。
比如:在父组件挂载完毕获取组件实例
父组件内部代码:
```
ref与$parent
```
但是需要注意,如果想让父组件获取子组件的数据或者方法需要通过defineExpose对外暴露,因为vue3中组件内部的数据对外“关闭的”,外部不能访问
```
```
$parent可以获取某一个组件的父组件实例VC,因此可以使用父组件内部的数据与方法。必须子组件内部拥有一个按钮点击时候获取父组件实例,当然父组件的数据与方法需要通过defineExpose方法对外暴露
```
```
### 1.7provide与inject
**provide[提供]**
**inject[注入]**
vue3提供两个方法provide与inject,可以实现隔辈组件传递参数
组件组件提供数据:
provide方法用于提供数据,此方法执需要传递两个参数,分别提供数据的key与提供数据value
```
```
后代组件可以通过inject方法获取数据,通过key获取存储的数值
```
```
### 1.8pinia
**pinia官网:https://pinia.web3doc.top/**
pinia也是集中式管理状态容器,类似于vuex。但是核心概念没有mutation、modules,使用方式参照官网
### 1.9slot
插槽:默认插槽、具名插槽、作用域插槽可以实现父子组件通信.
**默认插槽:**
在子组件内部的模板中书写slot全局组件标签
```
```
在父组件内部提供结构:Todo即为子组件,在父组件内部使用的时候,在双标签内部书写结构传递给子组件
注意开发项目的时候默认插槽一般只有一个
```
我是默认插槽填充的结构
```
**具名插槽:**
顾名思义,此插槽带有名字在组件内部留多个指定名字的插槽。
下面是一个子组件内部,模板中留两个插槽
```
todo
```
父组件内部向指定的具名插槽传递结构。需要注意v-slot:可以替换为#
```
slot
//可以用#a替换
填入组件A部分的结构
//可以用#b替换
填入组件B部分的结构
```
**作用域插槽**
作用域插槽:可以理解为,子组件数据由父组件提供,但是子组件内部决定不了自身结构与外观(样式)
子组件Todo代码如下:
```
```
父组件内部代码如下:
```
slot
{{$row.title}}
```
## 二、搭建后台管理系统模板
### 2.1项目初始化
今天来带大家从0开始搭建一个vue3版本的后台管理系统。一个项目要有统一的规范,需要使用eslint+stylelint+prettier来对我们的代码质量做检测和修复,需要使用husky来做commit拦截,需要使用commitlint来统一提交规范,需要使用preinstall来统一包管理工具。
下面我们就用这一套规范来初始化我们的项目,集成一个规范的模版。
#### 2.1.1环境准备
- node v16.14.2
- pnpm 8.0.0
#### 2.1.2初始化项目
本项目使用vite进行构建,vite官方中文文档参考:[cn.vitejs.dev/guide/](https://cn.vitejs.dev/guide/)
**pnpm:performant npm ,意味“高性能的 npm”。[pnpm](https://so.csdn.net/so/search?q=pnpm&spm=1001.2101.3001.7020)由npm/yarn衍生而来,解决了npm/yarn内部潜在的bug,极大的优化了性能,扩展了使用场景。被誉为“最先进的包管理工具”**
pnpm安装指令
```
npm i -g pnpm
```
项目初始化命令:
```
pnpm create vite
```
进入到项目根目录pnpm install安装全部依赖.安装完依赖运行程序:pnpm run dev
运行完毕项目跑在http://127.0.0.1:5173/,可以访问你得项目啦
可以在package.json文件中的中的scripts下面添加快捷指令,--open会在运行程序后自动打开
```json
"scripts": {
"dev": "vite --open",
},
```
### 2.2项目配置
#### 一、eslint配置
**eslint中文官网:http://eslint.cn/**
ESLint最初是由[Nicholas C. Zakas](http://nczonline.net/) 于2013年6月创建的开源项目。它的目标是提供一个插件化的**javascript代码检测工具**
首先安装eslint
```
pnpm i eslint -D
```
生成配置文件:.eslint.cjs
```
npx eslint --init
```
**.eslint.cjs配置文件**
```
module.exports = {
//运行环境
"env": {
"browser": true,//浏览器端
"es2021": true,//es2021
},
//规则继承
"extends": [
//全部规则默认是关闭的,这个配置项开启推荐规则,推荐规则参照文档
//比如:函数不能重名、对象不能出现重复key
"eslint:recommended",
//vue3语法规则
"plugin:vue/vue3-essential",
//ts语法规则
"plugin:@typescript-eslint/recommended"
],
//要为特定类型的文件指定处理器
"overrides": [
],
//指定解析器:解析器
//Esprima 默认解析器
//Babel-ESLint babel解析器
//@typescript-eslint/parser ts解析器
"parser": "@typescript-eslint/parser",
//指定解析器选项
"parserOptions": {
"ecmaVersion": "latest",//校验ECMA最新版本
"sourceType": "module"//设置为"script"(默认),或者"module"代码在ECMAScript模块中
},
//ESLint支持使用第三方插件。在使用插件之前,您必须使用npm安装它
//该eslint-plugin-前缀可以从插件名称被省略
"plugins": [
"vue",
"@typescript-eslint"
],
//eslint规则
"rules": {
}
}
```
##### 1.1vue3环境代码校验插件
```
# 让所有与prettier规则存在冲突的Eslint rules失效,并使用prettier进行代码检查
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
# 运行更漂亮的Eslint,使prettier规则优先级更高,Eslint优先级低
"eslint-plugin-prettier": "^4.2.1",
# vue.js的Eslint插件(查找vue语法错误,发现错误指令,查找违规风格指南
"eslint-plugin-vue": "^9.9.0",
# 该解析器允许使用Eslint校验所有babel code
"@babel/eslint-parser": "^7.19.1",
```
安装指令
```
pnpm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser
```
##### 1.2修改.eslintrc.cjs配置文件
```
// @see https://eslint.bootcss.com/docs/rules/
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true,
},
/* 指定如何解析语法 */
parser: 'vue-eslint-parser',
/** 优先级低于 parse 的语法解析配置 */
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true,
},
},
/* 继承已有的规则 */
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
plugins: ['vue', '@typescript-eslint'],
/*
* "off" 或 0 ==> 关闭规则
* "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
* "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
*/
rules: {
// eslint(https://eslint.bootcss.com/docs/rules/)
'no-var': 'error', // 要求使用 let 或 const 而不是 var
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-unexpected-multiline': 'error', // 禁止空余的多行
'no-useless-escape': 'off', // 禁止不必要的转义字符
// typeScript (https://typescript-eslint.io/rules)
'@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量
'@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
'@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
'@typescript-eslint/semi': 'off',
// eslint-plugin-vue (https://eslint.vuejs.org/rules/)
'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
'vue/script-setup-uses-vars': 'error', // 防止
```
在src文件夹目录下创建一个index.ts文件:用于注册components文件夹内部全部全局组件!!!
```
import SvgIcon from './SvgIcon/index.vue';
import type { App, Component } from 'vue';
const components: { [name: string]: Component } = { SvgIcon };
export default {
install(app: App) {
Object.keys(components).forEach((key: string) => {
app.component(key, components[key]);
})
}
}
```
在入口文件引入src/index.ts文件,通过app.use方法安装自定义插件
```
import gloablComponent from './components/index';
app.use(gloablComponent);
```
### 3.5集成sass
我们目前在组件内部已经可以使用scss样式,因为在配置styleLint工具的时候,项目当中已经安装过sass sass-loader,因此我们再组件内可以使用scss语法!!!需要加上lang="scss"
```
```
接下来我们为项目添加一些全局的样式
在src/styles目录下创建一个index.scss文件,当然项目中需要用到清除默认样式,因此在index.scss引入reset.scss
```
@import reset.scss
```
在入口文件引入
```
import '@/styles'
```
但是你会发现在src/styles/index.scss全局样式文件中没有办法使用$变量.因此需要给项目中引入全局变量$.
在style/variable.scss创建一个variable.scss文件!
在vite.config.ts文件配置如下:
```
export default defineConfig((config) => {
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/variable.scss";',
},
},
},
}
}
```
**`@import "./src/styles/variable.less";`后面的`;`不要忘记,不然会报错**!
配置完毕你会发现scss提供这些全局变量可以在组件样式中使用了!!!
### 3.6mock数据
安装依赖:https://www.npmjs.com/package/vite-plugin-mock
```
pnpm install -D vite-plugin-mock mockjs
```
在 vite.config.js 配置文件启用插件。
```
import { UserConfigExport, ConfigEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
import vue from '@vitejs/plugin-vue'
export default ({ command })=> {
return {
plugins: [
vue(),
viteMockServe({
localEnabled: command === 'serve',
}),
],
}
}
```
在根目录创建mock文件夹:去创建我们需要mock数据与接口!!!
在mock文件夹内部创建一个user.ts文件
```
//用户信息数据
function createUserList() {
return [
{
userId: 1,
avatar:
'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'admin',
password: '111111',
desc: '平台管理员',
roles: ['平台管理员'],
buttons: ['cuser.detail'],
routes: ['home'],
token: 'Admin Token',
},
{
userId: 2,
avatar:
'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'system',
password: '111111',
desc: '系统管理员',
roles: ['系统管理员'],
buttons: ['cuser.detail', 'cuser.user'],
routes: ['home'],
token: 'System Token',
},
]
}
export default [
// 用户登录接口
{
url: '/api/user/login',//请求地址
method: 'post',//请求方式
response: ({ body }) => {
//获取请求体携带过来的用户名与密码
const { username, password } = body;
//调用获取用户信息函数,用于判断是否有此用户
const checkUser = createUserList().find(
(item) => item.username === username && item.password === password,
)
//没有用户返回失败信息
if (!checkUser) {
return { code: 201, data: { message: '账号或者密码不正确' } }
}
//如果有返回成功信息
const { token } = checkUser
return { code: 200, data: { token } }
},
},
// 获取用户信息
{
url: '/api/user/info',
method: 'get',
response: (request) => {
//获取请求头携带token
const token = request.headers.token;
//查看用户信息是否包含有次token用户
const checkUser = createUserList().find((item) => item.token === token)
//没有返回失败的信息
if (!checkUser) {
return { code: 201, data: { message: '获取用户信息失败' } }
}
//如果有返回成功信息
return { code: 200, data: {checkUser} }
},
},
]
```
**安装axios**
```
pnpm install axios
```
最后通过axios测试接口!!!
### 3.7axios二次封装
在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候
我们经常会把axios进行二次封装。
目的:
1:使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)
2:使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)
在根目录下创建utils/request.ts
```
import axios from "axios";
import { ElMessage } from "element-plus";
//创建axios实例
let request = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000
})
//请求拦截器
request.interceptors.request.use(config => {
return config;
});
//响应拦截器
request.interceptors.response.use((response) => {
return response.data;
}, (error) => {
//处理网络错误
let msg = '';
let status = error.response.status;
switch (status) {
case 401:
msg = "token过期";
break;
case 403:
msg = '无权访问';
break;
case 404:
msg = "请求地址错误";
break;
case 500:
msg = "服务器出现问题";
break;
default:
msg = "无网络";
}
ElMessage({
type: 'error',
message: msg
})
return Promise.reject(error);
});
export default request;
```
### 3.8API接口统一管理
在开发项目的时候,接口可能很多需要统一管理。在src目录下去创建api文件夹去统一管理项目的接口;
比如:下面方式
```
//统一管理咱们项目用户相关的接口
import request from '@/utils/request'
import type {
loginFormData,
loginResponseData,
userInfoReponseData,
} 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(API.LOGIN_URL, data)
//获取用户信息
export const reqUserInfo = () =>
request.get(API.USERINFO_URL)
//退出登录
export const reqLogout = () => request.post(API.LOGOUT_URL)
```
## 四、项目的资源地址
贾成豪老师代码仓库地址:https://gitee.com/jch1011/vue3_admin_template-bj1.git
项目在线文档:
服务器域名:http://sph-api.atguigu.cn
swagger文档:
http://139.198.104.58:8209/swagger-ui.html
http://139.198.104.58:8212/swagger-ui.html#/
echarts:国内镜像网站
https://www.isqqw.com/echarts-doc/zh/option.html#title
http://datav.aliyun.com/portal/school/atlas/area_selector
## 五、项目学习个人笔记
### 5.1 零碎知识点
- Error 也是ts的内置对象,比如:
```ts
ElNotification({
type: 'warning',
message: (error as Error).message,
})
```
- 封装时间
```ts
export const getTime = () => {
let hours = new Date().getHours()
let message = ''
// 第一种写法 ifelse
if (hours <= 9) {
message = '早上'
} else if (hours <= 12) {
message = '上午'
} else if (hours <= 18) {
message = '下午'
} else {
message = '晚上'
}
// 第二种写法
let message = hours <= 9 ? '早上' : hours <= 14 ? '上午' : hours <= 18 ? '下午' : '晚上'
return message
}
```
### 5.2 css样式复习
#### 溢出文字设置为小点
```css
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
```
#### 滚动菜单的基本布局(element-plus)
```ts
```
一个 **el-menu** 就是一个菜单项,里面的二级菜单可以直接**el-sub-menu**,里面还可以继续放具名插槽和**el-menu-item-group**和**el-menu-item**
#### 路由组件过渡动画效果设置
查看官方过渡动效介绍:[过渡动效](https://router.vuejs.org/zh/guide/advanced/transitions.html)
```vue
```
##### 给具体某个局部设置过渡动效
```vue
```
### 滚动条效果设置
```scss
// 滚动条外观设置
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background-color: $base-menu-background;
}
::-webkit-scrollbar-thumb {
width: 10px;
background-color: yellowgreen;
border-radius: 10px;
}
```
## 六、vue3零碎知识
### 6.1 v-for 与 v-if
- `v-if` 具有更高的优先级
### 6.2 模拟刷新效果
本质是实现组件销毁与挂载的过程
```vue
```
### 6.3 报错处理
```ts
"getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?
// 解决方案 (推荐方案)
import pinia from '@/store'
let useStore = useUserStore(pinia)
// 另外这个报错无非就是提示在挂载app之前调用了仓库
// 故此我尝试使用setTimeout(()=>{let useStore = useUserStore()})
// 或者 使用nextTick(()=>{let useStore = useUserStore()}) 似乎都可以
```
## 七、全屏
> 只读属性 **Document.fullscreenElement** 返回当前页面中以全屏模式呈现的 Element,如果当前页面未使用全屏模式,则返回 null。
>
> > document.documentElement: 对于任何非空 HTML 文档,调用 document.documentElement 总是会返回一个 元素,且它一定是该文档的根元素。借助这个只读属性,能方便地获取到任意文档的根元素。
> >
> > > requestFullscreen: 用于发出异步请求使元素进入全屏模式。在调用 requestFullScreen() 之前,可以对 fullscreenchange 和 fullscreenerror 事件进行监听,这样在请求进入全屏模式成功或者失败时都能收到事件通知。
> > > document.exitFullscreen(); 退出全屏
### 7.1 请求用户数据
一般用户在登录的时候把用户的用户名,密码等身份信息收集起来发送给客户端,在登录成功后,下一次需要用到的时候,可以在请求拦截器中添加token,如config.headers.token = userStore.token,携带给客户端.公共的信息最好存储在仓库中.
### 7.2 再次登录回到之前的操作页面
在退出的时候可以,可以向路由$**route**中添加一个路由信息,下次登录的时候,判断是否有query参数,如果有就跳转到之前的页面,如果没有就跳转到首页.
```vue
```
#### 问题故障
- 未退出之前,不能访问登录页面
- 通过路由全局前置路由守卫来控制
```ts
router.beforeEach(async (to: any, from, next) => {
// 访问某一个路由之前的守卫
// to: 即将要进入的目标
// from: 当前导航正要离开的路由
// next: 路由放行函数,新版router不建议使用,当任然可以使用.
nprogress.start()
// 获取token,用来判断用户是否登录成功
let token = useStore.token
// 获取用户名字
let username = useStore.username
if (token) {
// 用户登录的判断,是不能再次访问login,让路劲指向首页
if (to.path == '/login') {
next({ path: '/' }) // 执行根路径layout,但是layout通过了redirect重定向到了二级路由首页
} else {....}}})
```
- 未登录之前,不能访问登录后才能访问的页面
- 也是通过全局前置路由守卫来处理
```ts
router.beforeEach(async (to: any, from, next) => {
if(token){
....
}else {
// 用户未登录的判断
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
})
```
- 登录后,用户信息保存在store中,仓库是用对象存储的,刷新页面丢失数据的情况
- 我们**最开始**采用的方式是在用户登录后,页面组件挂载的时候,发送请求获取用户的信息,但是这样一来,如果页面比较多,那么就需要在可以客户的每个页面都需要设置同类的代码,代码质量不高,且繁杂
```vue
```
- 其次我们还可以采用,全局路由守卫来帮帮我们操作,在访问页面的时候,有信息就放行,没有用户信息,发送请求,捞到数据后再放行.
- 进度条业务
```ts
// 路由鉴权,项目当中路由能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)
import router from './router'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'
// 全局守卫: 项目当中任意路由切换都会触发的钩子
// 全局前置守卫
router.beforeEach((to: any, from, next) => {
// 访问某一个路由之前的守卫
// to: 即将要进入的目标
// from: 当前导航正要离开的路由
// next: 路由放行函数,新版router不建议使用,当任然可以使用.
nprogress.start()
next()
})
// 全局后置守卫
router.afterEach((to, from, next) => {
nprogress.done()
})
// 第一个wenti:任意路由切换实现进度条的业务,使用插件 nprogress -- npm i nprogress
```
### 路由鉴权总结代码:
```ts
import { path } from 'path'
// 路由鉴权,项目当中路由能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)
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'
import pinia from '@/store'
let useStore = useUserStore(pinia)
// 全局守卫: 项目当中任意路由切换都会触发的钩子
// 全局前置守卫
router.beforeEach(async (to: any, from, next) => {
// document.title = setting.title + '-' + to.meta.title // 动态修改网页的title,便于个性化定制,最好放置在后置路由守卫中
// 访问某一个路由之前的守卫
// to: 即将要进入的目标
// from: 当前导航正要离开的路由
// next: 路由放行函数,新版router不建议使用,当任然可以使用.
nprogress.start()
// 获取token,用来判断用户是否登录成功
let token = useStore.token
// 获取用户名字
let username = useStore.username
if (token) {
// 用户登录的判断,是不能再次访问login,让路劲指向首页
if (to.path == '/login') {
next({ path: '/' }) // 执行根路径layout,但是layout通过了redirect重定向到了二级路由首页
} else {
// 登录成功,访问其余的几个路由
// 有用户信息
if (username) {
next() //放行
} else {
// 如果没有用户信息,在守卫这里发送请求获取到了用户信息再放心
try {
await useStore.userInfo()
// 获取用户信息成功,放行
next()
} catch (error) {
//登录成功,发送请求或者用户信息失败
// token 过期
// 用户手动修改了本地存储的token
// 退出登录-- 用户相关数据清空
// 注意: 本应该发送退出请求,成功后在跳转到登录页面,但是本项目没有退出接口,故此为做操作,下面为模拟
useStore.userLogout()
next({ path: '/login', query: { redirect: to.path } })
}
}
}
} else {
// 用户未登录的判断
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
})
// 全局后置守卫
router.afterEach((to, from, next) => {
document.title = setting.title + '-' + to.meta.title // 动态修改网页的title,便于个性化定制
nprogress.done()
})
// 第一个wenti:任意路由切换实现进度条的业务,使用插件 nprogress -- npm i nprogress
// 第二个问题: 路由鉴权(路由访问权限的设置)
// 全部路由: 登录 | 404 | 任意路由 | 测试路由 | 首页 | 数据大屏 | 权限管理(3个子路由) | 商品管理 (4个子路由)|
// 用户未登录: 可以访问login,其余的七个路由不能访问(指向login)
// 用户登录成功: 不可以访问login(指向首页),其余的路由可以访问
```
## 八、功能内容开发
### 8.1 品牌管理
- 在使用**element-plus**ui库的时候,需要注意:
1. 对话框差价的数据回显可以通过标签体内的默认插槽,把数据传递给使用者,如:
```vue
编辑
删除
```
2. 在修改和添加接口方法类似的情况,可以把请求数据放在 一个**api**中,通过特有的数据进行判断,比方说添加数据不需要id,而修改数据需要id,如:
```ts
//添加与修改已有品牌接口方法
export const reqAddOrUpdateTrademark = (data: TradeMark) => {
if (data.id) {
// 修改已有品牌的数据
return request.put(API.UPDATETRADEMARK_URL, data)
} else {
// 新增品牌
return request.post(API.ADDTRADEMARK_URL, data)
}
}
```
3.在使用同一个接口,而会产生多种请求的方式的时候,则需要区别处理,如果需要停留在当前页面,可以在更新数据的时候,传入当前的页码,如:
```ts
// 对话框底部确认按钮
async function confirm() {
try {
let result: any = await reqAddOrUpdateTrademark(TrademarkParams)
if (result.code == 200) {
// 添加成功
ElMessage({
type: 'success',
message: TrademarkParams.id ? '修改品牌成功' : '添加品牌成功',
})
// 回溯数据清空
// Object.assign(TrademarkParams, { tmName: '', logoUrl: '' }) // 放在打开按钮的时候更好
} else {
// 添加失败
ElMessage({
type: 'warning',
message: TrademarkParams.id ? '修改品牌失败' : '添加品牌失败',
})
}
} catch (error) {
ElMessage({ type: 'error', message: '网络故障,请稍后再试' })
} finally {
// 关闭对话框
dialogFormVisible.value = false
// 获取最新一次新的数据,同时根据有无id判断是添加还是修改,
// 如果是修改则回到当前页面
getHasTrademark(TrademarkParams.id ? pageNo.value : 1)
}
}
```
4. 防止用户在添加数据之前,修改了数据,故此需要在每次打开对话框之前,清空原先的数据,这样比分布写在每一个请求当中,更高效,如:
```ts
// 控制商品添加的的对话框的函数
const addTrademark = () => {
// 对话框显示
dialogFormVisible.value = true
// 清空旧数据,或者废弃数据,放在这里只用清空一次
TrademarkParams.id = undefined // 此处清空id的目的在于防止用户在添加之前点击了修改数据,而对话框数据不变化的现象,可以是 0
TrademarkParams.logoUrl = ''
TrademarkParams.tmName = ''
}
```
5. **element-plus** 中的自定义表单校验规则
- 是在表单项 添加rules规则
- 通过自定义rules规则,形式如下
```ts
// 获取el-form 组件实例
const formRef = ref()
// 自定义商品名字校验规则的方法
const validatorTmName = (rule: any, value: any, callBack: any) => {
// 当表单元素触发校验时,会触发此方法
// callBack: 放行函数; value:就是表单输入的值
// 自定义校验规则
if (value.trim().length >= 2) {
callBack()
} else {
// 校验未通过返回的错误提示信息
callBack(new Error('品牌名称的长度应该大于等于两位'))
}
}
// 自定义商品图片的校验规则
const validatorLogoUrl = (rule: any, value: any, callBack: any) => {
// 当上传的是图片时,value就是上传成功的图片地址
// 如果图片上传,有地址,则为真
if (value) {
callBack()
} else {
callBack(new Error('LOGO图片务必上传'))
}
}
// 表单校验规则对象
const rules = {
// required: 这个字段代表校验的务必性,标识就是页面的选项前有个五角星
// trigger: 验证逻辑的触发方式 ,'blur' | 'change'
// message: 触发验证时的提示消息
// validator: 验证器
tmName: [{ required: true, trigger: 'blur', validator: validatorTmName }],
logoUrl: [{ required: true, validator: validatorLogoUrl }], // 应为这个一张图片,所以校验规则触发时机,不受trigger的限制
}
```
- 实在 上添加 **prop="tmName"** 属性来达到分别校验的目的.
- 对于图片类上传,其触发时机不受**trigger**影响,应该在是在提交表单的时候触发 **validate**属性后对应的回调,如:
```ts
// 自定义商品图片的校验规则
const validatorLogoUrl = (rule: any, value: any, callBack: any) => {
// 当上传的是图片时,value就是上传成功的图片地址
// 如果图片上传,有地址,则为真
if (value) {
callBack()
} else {
callBack(new Error('LOGO图片务必上传'))
}
}
// 表单校验规则对象
const rules = {
// 其它代码
logoUrl: [{ required: true, validator: validatorLogoUrl }], // 应为这个一张图片,所以校验规则触发时机,不受trigger的限制
}
async function confirm() {
// 在发送请求之前需要对整个表单进行校验
await formRef.value.validate() // 返回的是一个Promise,有成功有失败,我们必须要全部校验成功后才让发起请求
}
```
6. 清理对应的错误提示,特别是在添加和编辑的时候,在添加的时候应该注意,第一次并没有加载到组件实例,故此会报错,可以通过js的可以选操作法?.来处理,或者通过nextTick(),或者定时器setTimeout(),总是这就是一个处理**异步数据**的问题.
7. 表单中的数据都是通过数据双向绑定**v-model**来绑定的,故此在请求的是需要可以存储一下数据,便于下一次在其它地方用到,特别是在处理组件挂载**onMounted**中发请求获取数据,用于渲染.
8. 上传文件功能 中的cb2回调应该用来限制数据类型或者带下等,cb2的回调用来提交数据.