# vue3Elementplus
**Repository Path**: heecer/vue3-elementplus
## Basic Information
- **Project Name**: vue3Elementplus
- **Description**: 一个使用vite+vue3+elementPlus
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2023-10-19
- **Last Updated**: 2023-10-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
#### 1.0 项目涉及技术
```js
1. Vue3 最新setup语法糖
2. Vite 构建工具
3. Vuex + VueRouter
4. Windicss框架
5. ElementPlus组件库
6. VueUse工具库
7. axios
```
##### 1.1 构建项目
使用 `vite` 进行项目构建
```js
npm create vite@latest my-vue-app -- --template vue
```
##### 1.2 引用 element-plus
```js
1. npm install element-plus --save
2. 在main.js中引入完整包
```
##### 1.3 引用 Windi-CSS
```js
1. npm i -D vite-plugin-windicss windicss
2. vite.config.js 在vite配置中引入插件
3. main.js中引入
4. vscode 安装windicss插件
```
```html
.btn{ @apply bg-purple-500 px-4 py-2; }
```
##### 1.4 引用 vue-router
```js
1. npm install vue-router@4
2. 在main.js 引入router
3. 配置router.js
```
##### 1.5 别名配置
```js
export default defineConfig({
resolve: {
// @ == src目录层级
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
})
```
#### 2.0 知识点
##### 2.1 setup 语法糖和组合式 api
```vue
1. setup
在script中标签使用,那么声明的顶层(变量,函数以及import引入内容)的绑定都能在模板中直接使用
{{ count }}
{{ form.number }}
```
##### 2.4 动态路由的实现
```js
1. 拿到后端返回的路由数据,递归的去拿到路径,然后跟前端定义的去匹配,如果匹配到了,那么拿出来存放在数组里,如果遇到存在的路由直接跳过,不存在才添加到数组
```
##### 2.5 父子传值
> defineProps:子组件接收父组件传来的值
>
> defineEmits:子组件接收父组件传来的方法
>
> defineExpose:子组件暴露自己的属性或方法(打开关闭,开启关闭 loading)
```js
// defineProps+defineEmits
//父组件
//子组件
```
```js
// defineExpose
// 父组件
// 子组件
```
##### 2.5.1 v-moel 绑定 props 值
```js
把父组件中的传递的值,直接在 子组件中展示
// 父组件
// 子组件
确认
const props = defineProps({
modelValue: [String, Array]
})
const emit = defineEmits(['update:modelValue'])
function submit() {
if (imgArr.value.length) {
emit("update:modelValue", imgArr.value[0])
}
dialogVisible.value = false
}
```
##### 2.6 多个选择项,选择当前被点亮
> v-for 遍历循环数据,设置默认的点亮按钮,当点击其他 按钮去判断里面的唯一值是否与选择按钮一致,赋值给 checked 属性
>
> 设置一个变量,赋值给默认按钮具备的属性值,点击按钮时,把点击按钮的数组值赋给变量,然后判断 checked 属性值 是否相等,相等的为 true,则只有唯一一个点亮
```js
{{item.text}}
const isCheck = ref('week')
const optionList = [{
text: '近一小时',
value: 'hour',
}, {
text: '近一周',
value: 'week',
}]
function checkItem(item) {
isCheck.value = item.value
}
```
##### 2.7 获取高度
```
window.innerHeight || document.body.clientHeight
```
##### 2.8 抽离组件复用
```
哪个模块会被复用,那么就抽离哪一个模块区域,其他的不需要抽离
```
##### 2.9 数据提交的头部设置
```js
1. application/x-www-form-urlencoded
使用表单提交,请求方式是post,会把参数组成键值对的方式:name=zs&age=15
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
}
2. multipart/form-data
一般在文件上传时使用。表单提交方式,请求方式是post
headers: {
'Content-Type': 'multipart/form-data;charset=utf-8'
}
3. application/json
json格式,前后的的数据交互使用最多格式
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
```
##### 2.10 获取当前取得的值
> ` @click="change($event,row)"` event 可以获取当前对象所处的值,即 0/1
```html
```
##### 2.11 post-get请求参数配置
```js
1. 路径参数 + post-body参数
export function editImageList(id, data) {
return service({
url: `/admin/image_class/${id}`,
method: 'post',
params:data
})
}
2. 路径参数 + get-query参数
export function getManage(page, data) {
return service({
url: `/admin/manager/${page}`,
method: 'get',
params: data
})
}
```
2. ##### 2.13 导出与导入
```js
// 使用默认导出,当只需要导出一个内容时使用
const seaRouter = []
export default seaRouter
// 使用多个导出项,则不能默认default导出
export const seaRouter = []
export const barRouter = []
```
```js
// 使用默认引入
import seaRouter from './module'
// 使用多导出项引入
import {seaRouter,barRouter} from './module'
```
##### 2.12 el-submenu折叠失效,文字和>显示
失败原因:在el-menu中使用了组件化展示数据,官方规定el-menu需要直接存在,,,所以会存在折叠失效的问题
```vue
```
解决方法:1. 不用子组件展示,把子组件内容放在el-menu中;2. 使用css样式解决
```css
/*隐藏文字*/
.el-menu--collapse .el-sub-menu__title span {
display: none;
}
/*隐藏 > */
.el-menu--collapse .el-sub-menu__title .el-sub-menu__icon-arrow {
display: none;
}
```
##### 2.13 下拉框点击出现黑框
解决办法加上这个样式
```css
:deep(:focus-visible) {
outline: none;
}
```
#### 3.0 请求封装
> 1. 配置跨域问题
> 2. 请求拦截器 + 添加请求头部token
> 3. 响应拦截器 + 对数据进行简化 + 对错误进行处理
```js
import axios from 'axios'
import store from '@/store'
import { getMessage } from '@/utils/base'
import { getCookie } from '@/utils/token'
const service = axios.create({
baseURL: '/api',
})
// 请求拦截器 添加请求头部token
service.interceptors.request.use(
function (config) {
// 在发送请求前做点什么
const token = getCookie()
if (token) {
config.headers['token'] = token
}
// 一定要返回才会进行下一步,没有返回值不会请求了
return config
},
err => {
// 对错误返回
return Promise.reject(err)
}
)
// 响应拦截器
service.interceptors.response.use(
function (response) {
// 对响应数据做点什么
// 一定要返回
return response.data.data
},
err => {
// 对错误返回
const msg = err.response.data.msg || '请求失败'
if (msg == '非法token,请先登录!') {
store.dispatch('logout').finally(() => {
// 浏览器自带刷新
location.reload()
})
}
getMessage(msg, 'error')
return Promise.reject(err)
}
)
export default service
```
#### 4.0 **路由权限**
##### 1.0 菜单三层循环
1. 固定的三层之内数据
```vue
```
2. 不限菜单层级,一直循环
```vue
```
```vue
{{menu.name}}
{{menu.name}}
```
```json
// 数据参考值
{
id: 622,
rule_id: 122,
status: 1,
create_time: '2019-08-11 13:36:36',
update_time: '2021-12-21 19:37:11',
name: '首页',
desc: 'shop_goods_list',
frontpath: '/',
condition: null,
menu: 1,
order: 2,
icon: 'shopping-bag',
method: 'GET',
child: [],
},
{
id: 5,
rule_id: 0,
status: 1,
create_time: '2019-08-11 13:36:09',
update_time: '2021-12-21 19:31:11',
name: '后台面板',
desc: 'index',
frontpath: null,
condition: null,
menu: 1,
order: 1,
icon: 'help',
method: 'GET',
child: [
{
id: 10,
rule_id: 5,
status: 1,
create_time: '2019-08-11 13:37:02',
update_time: '2021-12-21 20:21:23',
name: '主控台',
desc: 'index',
frontpath: null,
condition: null,
menu: 1,
order: 1,
icon: 'help',
method: 'GET',
child: [
{
id: 101,
rule_id: 51,
status: 1,
create_time: '2019-08-11 13:37:02',
update_time: '2021-12-21 20:21:23',
name: '操作室1',
desc: 'index',
frontpath: '/goods/list',
condition: '',
menu: 1,
order: 20,
icon: 'home-filled',
method: 'GET',
child: [],
},
{
id: 1011,
rule_id: 511,
status: 1,
create_time: '2019-08-11 13:37:02',
update_time: '2021-12-21 20:21:23',
name: '操作室2',
desc: 'index',
frontpath: '/namePage',
condition: '',
menu: 1,
order: 20,
icon: 'home-filled',
method: 'GET',
child: [],
},
],
},
{
id: 1622,
rule_id: 1122,
status: 1,
create_time: '2019-08-11 13:36:36',
update_time: '2021-12-21 19:37:11',
name: '2222首页',
desc: 'shop_goods_list',
frontpath: '/',
condition: null,
menu: 1,
order: 2,
icon: 'shopping-bag',
method: 'GET',
child: [],
},
],
},
```
##### 2.0 菜单权限
1. 点击左侧 ,路由进行跳转
```vue
```
2. **动态渲染**,依据本地路由配置,与用户路由信息做比较,存在的则添加到路由中进行菜单展示
```js
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/layout/admin.vue'
// 静态路由
const routes=[{
path: '/',
name: 'admin',
component: Layout,
},
{
path: '/login',
name: 'Login',
meta: {
title: '登录页',
},
component: () => import('@/views/login/index.vue'),
},]
// 动态路由
const asyncRoutes = [{
path: '/',
name: 'HomePage',
component: () => import('@/views/homePage/index.vue'),
meta: {
title: '后台首页',
icon: 'user',
},
},
{
path: '/goods/list',
name: 'GoodList',
component: () => import('@/views/goodsPage/index.vue'),
meta: {
title: '商品管理',
icon: 'user',
},
},
]
// 导出静态路由
export const router = createRouter({
history:createWebHashHistory(),
routes
})
// 导出动态路由
export function addRoutes(menus){
let hasNewRoute = false
cosnt findAndAddRoute = arr => {
arr.forEach(e => {
let res = asyncRoutes.find(item => {
return item.path == e.frontpath
})
// 存在相匹配的路由,并且路由不存在当前路由list中
if(res && !router.hasRoute(res.path)){
// 这个admin需要与 静态路由中 路径为 / 的name一致
router.addRoute('admin',res)
hasNewRoute = true
}
// 如果存在子路由,则递归调用
if(e.child && e.child.length>0){
findAndAddRoute(e.child)
}
})
}
findAndAddRoute(menus)
// 用来控制是否循环调用
return hasNewRoute
}
```
3. 调用封装好的动态路由
```js
// 添加动态路由方法已经写好,怎么去调用这个方法呢,需要在路由守卫中去判断,存在token的时候,去调用获取动态路由
let hasGetInfo = false
let hasNewRoute = false
if(token && !hasGetInfo){
let {menus} = await store.dispatch('getInfo')
hasGetInfo = true
hasNewRoute = addRoutes(menus)
}
hasNewRoute ? next(to.fullPath) : next()
```
##### 3.0 菜单与tab标签联动
1. 点击左侧菜单栏,tab增加对应选项卡,以及高亮
点击左侧菜单栏,获取对应的路径跟title,然后添加tab,把选项卡设置高亮
```vue
```
2. 来回切换tab选项卡,跳转到对应页面更新路由,以及高亮该选项卡
```js
// 启用 changeTab方法
const changeTab = (path) => {
activeTab.value = path
router.push(path)
}
```
3. 切换tab的同时,左侧的菜单也要对应展开
想要实现菜单栏跟tab选项卡对应展开,那么路由路径是中间联系人,都需要与路径值相等
```js
// 在menu.vue 子组件中 使用组件内的守卫
onBeforeRouteUpdate((to,from) => {
defaultActive.value = to.path
})
```
4. 点击删除tab,删除该选项卡
```js
const removeTab = path => {
let tab = tabList.value
let a = activeTab.value
if(a == path){
tab.forEach((t,index) => {
// 删除当前项,那么展示后一项,或者前一项
if(t.path == path){
const nextTab = tab[index+1] || tab[index-1]
if(nextTab){
a = nextTab.path
}
}
})
}
activeTab.value = a
// 过滤掉删除的路径
tabList.value = tabList.value.filter(item => {
return item.path != path
})
// 缓存
setTabList(tabList.value)
}
```
##### 4.0 按钮权限
根据用户角色的不同,展示不同的按钮功能
> 1. 登录时获取用户拥有的权限规则,缓存在vuex中
> 2. 使用自定义方法 directive,调用是否存在值方法
> 3. 定义方法,判断改按钮权限是否存在用户按钮权限中,不存在那么该用户不具备这个功能,直接删除该节点,存在则具备不改动
```js
// permission.js
import store from '@/store'
function hasPermission(value, el) {
if (!Array.isArray(value)) {
throw new TypeError('需要配置权限组')
}
let res = value.findIndex(item => {
// 登录后,获取用户时缓存权限规则
return store.state.ruleNames.includes(item)
})
// 如果不存在该路由,那么找到父节点,删除父节点下的子节点 即自己
if (el && res == -1) {
el.parentNode && el.parentNode.removeChild(el)
}
return res
}
export default {
install(app) {
app.directive('has', {
mounted(el, binging) {
hasPermission(binging.value, el)
},
})
},
}
// main.js
import permission from '@/directives/permission.js'
app.use(permission)
```
```vue
新增
新增
```
##### 5.0 全局路由守卫
> 每次进行路由切换的时候,需要进行路由验证,1 权限验证、2 token验证、3 显示加载线条
>
> 1. 存在token,跳转到登录页,提示请勿重复登录
> 2. 不存在toekn,不在登录页,跳转到登录页
> 3. 存在token,不存在路由菜单信息,那么动态添加路由
> 4. 进入前置钩子,启动加载动画,后置钩子,关闭加载动画
```js
/*
* 全局路由守卫处理登录问题,权限验证
* 1. 清除token,那么回到登录页
* 2. beforeEach 参一 即将要去的页面;参二 当前页面;参三 需要执行,才会有后续页面
*/
import { router, addRoutes } from '@/router'
import { getMessage, showLoading, hideLoading } from '@/utils/base'
import { getCookie } from '@/utils/token'
import store from '@/store'
// let hasGetInfo = false
router.beforeEach(async (to, from, next) => {
showLoading()
const token = getCookie()
if (!token && to.path != '/login') {
getMessage('请重新登录', 'error')
return next({
path: '/login',
})
}
if (token && to.path == '/login') {
getMessage('请勿重复登录', 'error')
return next({
path: from.path ? from.path : '/',
})
}
// 如果存在token,那么就获取用户最新信息
let hasNewRoute = false
// if (token && !hasGetInfo) {
if (token) {
if (store.state.menus.length === 0) {
let { menus } = await store.dispatch('getInfo')
// hasGetInfo = true
// 调用添加动态路由
hasNewRoute = addRoutes(menus)
}
}
// 设置页面标题
let title = (to.meta.title ? to.meta.title : '') + 'Vue3'
document.title = title
hasNewRoute ? next(to.fullPath) : next()
})
// 全局后置钩子
router.afterEach((to, from) => {
hideLoading()
})
```
#### 5.0 组件通信
##### 1. defineProps
> 在 `template`模板中可以直接使用,在 `script` 模板中需要 props.name 使用
```vue
{{money}}{{info}}
```
##### 2. defineEmits
> 在vue3 中使用组合式api ,不存在this,所以不能用 this.$emit() 去传递方法和数据
>
> 子组件绑定的事件,都是自定义事件。不然大多为DOM事件
```vue
// 父组件
// 子组件
```
##### 3. 全局事件总线 mitt
> 兄弟之间传递,类似发布订阅
```js
// bus.js 全局 npm install mitt
// mitt是一个方法,执行会返回 bus 对象。on 接收数据; emit 发送数据;all未知;off未知;
import mitt from 'mitt'
const bus = mitt()
export default bus
```
```vue
我是哥哥
```
```vue
我是弟弟
```
##### 4. v-model
> 组件通信,父子组件数据同步
>
> v-model在组件身上使用:1.相当于给子组件传递props[modelValue];2.相当于给子组件绑定自定义事件update:modelValue
```vue
// 父组件
// 子组件
```
```vue
// 父组件
// 子组件
```
##### 7. useAttrs
> 获取组件标签身上的 属性和方法,类似props。如果使用defineProps取值,那么useAttrs就取不到
```js
// useAttrs
import {useAttrs} from 'vue'
let $attrs = useAttrs()
// 该方法执行会返回一个对象,包含属性跟方法,如果该属性被defineProps截获,那么$attrs找不到
```
##### 8. ref + defineExpose、$parent + defineExpose
> ref + defineExpose 可以快捷让 **父组件拿到子组件**的属性和方法,子组件内部数据对外默认关闭,需要使用 defineExpose方法对外暴露相应的值
>
> defineExpose --- defineEmit
>
> 两个都能把子组件方法暴露给父组件;前者暴露非操作方法,后者暴露点击操作方法。
```vue
子组件-曹植
```
> $parent + defineExpose 可以让 子组件快速拿到父组件的属性和方法,也需要父组件暴露对应的属性和方法
```vue
```
##### 9. provide+inject 3代快速通信
```js
// provide 祖先给后代提供数据,参数一 提供数据的key,参数二 提供数据值
import {ref, provide} from 'vue'
let car = ref('法拉利')
provide('carName',car)
```
```js
// 孙子组件
import {inject} from 'vue'
let car = inject('carName')
console.log(car.value) // 法拉利
```
##### 10. pinia
> vuex 实现任意组件之间通信、核心:state、getters、mutations、actions、modules
>
> oinia 实现任意组件之间通信、核心:state、actions、getters
```js
// pinia写法:选择器API、组合式API store.js
import {createPinia} from 'pinia'
// 大仓库
```
```js
// 小仓库 info.js
import {defineStore} from 'pinia'
// 参一:小仓库名字,参二:小仓库配置对象。方法会返回一个函数,函数作用让组件获取到仓库数据
let useInfoStore = defineStore('info',{
// 存储数据:state
state:() => {
return {count:99}
},
// 修改数据方法
actions:{
updateNum(){
console.log('更新数据')
this.count++
}
},
// 计算属性
getters:{
}
})
export default useInfoStore
```
```js
// mian.js
import store from './store'
app.use(store)
```
#### 6.0 需求难点
##### 1.0 左右内容 各自滚动
大盒子内,左边类目名超出可滚动,右边类目值超出可滚动
```vue
```
实现思路:
左侧主体给固定宽度,需要内容出现滚动,尾部出现上页下页按钮;父级绝对定位,上下子元素 相对定位,上元素上左右为0、overflow-y:auto;下元素下左右为0;
右侧主体无需固定宽度,需要内容出现滚动,尾部出现上页下页按钮,一样设置;父级绝对定位,上下子元素 相对定位,上元素上左右为0、overflow-y:auto;下元素下左右为0;
```css
```
##### 2.0 优化全局滚动条样式
```html
// app.vue
```
##### 3.0