# 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引入内容)的绑定都能在模板中直接使用 ``` ##### 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 ``` ```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 ``` ##### 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