难点
菜单权限实现
菜单权限就是在登录请求中,获取到权限数据,根据相应的数据,展示菜单信息,点击菜单后,才能看到相关的页面,理解成将页面与理由进行解耦。
实现菜单权限主要有两种方式
方案一:
菜单与路由分离,菜单由后端返回,前端定义路由信息,路由信息中必须有name字段,后端返回的菜单信息中也必须有name字段,根据两者的name字段做唯一性校验。
实现方法:成功登录之后把后端菜单数据持久化存储,当进行菜单切换时,在全局路由守卫里面进行判断,看要跳转到的路由name是否存在于后端菜单数据里面,如果存在,就允许跳转,如果不存在,就跳转到自定义403页面。
缺点:菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用。
全局路由守卫里,每次路由跳转都要做判断
方案二
菜单和路由都由后端返回,前端统一定义路由组件,通过addRoutes进行动态挂载,将component字段换为真正的组件。
这个项目采用的方案二。
实现方法:
1.登录成功后,根据用户信息调用菜单权限接口获取菜单数据,把菜单数据传到pinia持久化存储定义的action中进行处理
menuPermissions().then(({data})=>{
menuStore.dynamicMenu(data.data)
})
2.在action中,要配置好route的component,进行页面渲染
async dynamicMenu(payload) {
//通过glob导入文件
const modules = import.meta.glob('../views/**/**/*.vue')
function routerSet(router) {
//遍历数据
router.forEach(route => {
if (!route.children) {
//获得url
const url = `../views${route.meta.path}/index.vue`
//通过url匹配包
route.component = modules[url]
} else {
//递归
routerSet(route.children)
}
});
}
routerSet(payload)
//拿到完整的路由数据
this.routerList = payload
// console.log(this.routerList)
},
其中,import.meta.glob是 ES 模块规范中的一个特性,接受俩参数,第一个参数是必须的,是一个字符串,用于指定模块路径的匹配模式。第二个参数是可选的,是一个对象,可用于配置模块导入的一些行为。
import.meta.glob返回对象形式,例如:
{
"../views/home.vue": () => import("../views/home.vue")
}
最后把路由数据进行持久化处理
3.动态导入
toRaw(routerList.value).forEach(item => {
router.addRoute('main',item)
});
动态导入到main下
注:通过toRaw获取原始对象,可以确保传递给router.addRoute的是最原始、最纯净的数据,避免因为响应式包装而可能带来的一些数据结构或行为上的差异,提高性能。
问题:刷新页面菜单消失
原因:因为菜单数据是登录之后才获取到的, 获取菜单数据之后,就存放在pinia中 ⼀旦刷新界⾯, pinia中的数据会重新初始化
解决:需要将权限数据存储在localStorage中,数据和pinia中的一致,当刷新完成后,数据从localStorage中获取
//刷新后的动态路由添加
const localDate = localStorage.getItem('ak_gl')
const menuStore = useMenuStore()
if(localDate){
menuStore.dynamicMenu(JSON.parse(localDate).routerList)
menuStore.routerList.forEach(item => {
router.addRoute('main',item)
});
}
总结
菜单权限的难点在于如何处理后端数据,正确的设置好route的component,这是最关键也是最难的一步,在动态导入路由的时候怎么解决不必要的性能开销。
用jsonp解决跨域
通常用配置代理的方法解决跨域,但是在这个项目中,用到了某某地图的接口产生跨域,所以用jsonp的方式进行解决
实现方法:
1.在main.js中全局配置
//先导入vue-jsonp
import { jsonp } from 'vue-jsonp';
//进行全局挂载
app.config.globalProperties.$jsonp= jsonp
2.在页面使用
import { getCurrentInstance} from 'vue';
//获取到实例
const { proxy } = getCurrentInstance();
//直接使用
proxy.$jsonp(url, data).then((res) => {}
难点在于如何把jsonp配置到全局之中,如何调用
顶部导航栏的删除
项目中有这样一个功能,当点击侧边栏的菜单时,会把点击的菜单名称添加到顶部导航栏,当叉掉当前所选菜单或者其他菜单时,根据逻辑操作顶部导航栏菜单的变化
增加菜单:
所有的激活菜单都要进行pinia状态管理
所以,我们要先在action中定义一个添加菜单的方法
//添加顶部菜单
//如果当前selectMenu内存在被点击的菜单,就不用添加,否则就添加
addMenu(payload) {
if (!this.selectMenu.some(item => item.path === payload.path)) {
this.selectMenu.push(payload);
}
}
在侧边栏页面中调用addMenu方法
const handelClick =(item,active)=>{
menuStore.addMenu(item.meta)
}
删除菜单
点击查号会关闭页面
如果删除的不是当前所选的页面,那么直接在selectMenu中删除
如果删除的是所选的页面,那么,后续该怎样进行页面的切换,这是一个难点
实现方法:
//点击关闭tag
const closeTab =(item,index)=>{
menuStore.closeMenu(item)
//删除非当前页,在store中直接删除
if(route.path !== item.path){
return
}
//获取菜单数据
const selectMenuDate=selectMenu.value
//删除的选中项是最后一项
if(index === selectMenuDate.length){
//可能会有人会说为什么和length相比,而不是length-1
//因为index是从0开始的,刚开始的时候我也和lengh-1相比的,发现没有效果,然后我又检查前面的代码
//发现menuStore.closeMenu(item)已经先在selectMenuDate删除了
//所以这里进行判断的时候,selectMenuDate是已经删除过的
//如果tag只有一个元素,即length已经变成0了
if(!selectMenuDate.length){
//跳到默认页面
router.push('/')
}else{
//往前移动
router.push({
path:selectMenuDate[index-1].path
//当前位置变为原来删除选项的index,要往前移动,index-1即可
})
}
} else {
//如果选中项位于中间位置,往后移动
router.push({
path:selectMenuDate[index].path
//同样道理,index已经变成了原来选项所在的位置
//删除后后面的选项往前移动,占据当前的index
})
}
}
侧边栏菜单与顶部菜单高亮一致
在测试阶段中,发现侧边栏菜单与顶部导航菜单高亮不一致,在菜单删除,添加,切换的时候,侧边栏菜单高亮纹丝不动,经过反复测试,发现当顶部导航菜单转换的时候,active没有更新,但是后端里并没有关于active更新的操作,这时候就需要前端开发人员自行完成
实现方法:
1.侧边栏的高亮效果是通过<el-menu中的:default-active实现的
default-active="menuActive"
所以要进行状态管理,在pinia的state中定义menuActive
2.实现更新操作
先在action中定义一个更新menuActive的方法
//更新active
updateMenuActive(payload) {
this.menuActive = payload;
}
在顶部栏操作页面
页面较少的情况下,我们可以这样操作,通过对象键值对的方法获取到状态码,即路由的index
const getActive = (path)=>{
const obj = {
'/dashboard':'1-1',
'/auth/admin':'1-2-1',
'/auth/group':'1-2-2',
'/vppz/staff':'1-3-1',
'/vppz/order':'1-3-2'
}
return obj[path]
}
// 在组件setup函数中使用onBeforeRouteUpdate导航守卫
onBeforeRouteUpdate((to, from) => {
//获取当前的路由path,执行store里面的更新active和menudate操作
menuStore.updateMenuActive(getActive(to.meta.path))
});
虽然看起来并不怎么难,但要想到用什么方式进行更新
亮点
二次封装local
二次封装local,提高安全性
因为这个项目有pc端和移动端两种,为了避免移动端的localStorage和pc端的localStorage冲突,对pc端中的localStorage二次封装
//对localstorge实现二次封装,提高安全性
const namespace = 'information'
export default {
setItem(key, val) {
//调用 getStorage 方法获取当前存储的数据对象
let storage = this.getStorage()
//然后将新的键值对添加到该对象中
storage[key] = val
//最后将新的对象转换为字符串并存储到 localStorage 中,即这种形式:namespace{{"key":"val"}}
window.localStorage.setItem(namespace, JSON.stringify(storage))
},
getStorage() {
try {
// 获取localStorage 中namespace里数据,如果没有返回空对象
return JSON.parse(window.localStorage.getItem(namespace) || '{}');
} catch (e) {
// 如果解析失败,返回空对象
console.error(e);
return {};
}
},
getItem(key) {
return this.getStorage()[key]
},
clearItem(key) {
let storage = this.getStorage()
delete storage[key]
window.localStorage.setItem(namespace, JSON.stringify(storage))
},
//清除local
clearAll() {
window.localStorage.removeItem(namespace)
}
}
数据可视化展示
在首页中,使用echart对订单数据进行可视化展示
组件高度复用
在移动端进行订单创建时,虽然每个功能不一样,但是页面中大部分展现情况还是一样,只用编写一个创建页面的组件,在跳转到创建页面,根据每个功能的需要对组件相关内容展示或者隐藏,实现组件的高度复用,提升开发效率
懒加载提高用户体验
在vue3种,懒加载实现的方式有很多种,例如各种UI框架内封装的懒加载组件,或者自己写一个懒加载的组件进行展示。
这个项目中使用第三方库vue-content-loader+svg绘制网站
vue-content-loader的好处是可以根据页面情况进行绘制,实现自定义的效果
<template>
<content-loader
viewBox="0 0 400 760"
:speed="2"
primaryColor="#f3f3f3"
secondaryColor="#ecebeb"
>
<rect x="13" y="7" rx="0" ry="0" width="140" height="35" />
<rect x="176" y="8" rx="0" ry="0" width="259" height="31" />
<rect x="14" y="64" rx="0" ry="0" width="384" height="37" />
<rect x="15" y="124" rx="0" ry="0" width="376" height="92" />
<circle cx="51" cy="268" r="38" />
<circle cx="191" cy="301" r="6" />
<circle cx="147" cy="267" r="38" />
<circle cx="243" cy="267" r="38" />
<circle cx="342" cy="266" r="38" />
<rect x="15" y="333" rx="0" ry="0" width="156" height="91" />
<rect x="221" y="329" rx="0" ry="0" width="156" height="91" />
<rect x="32" y="445" rx="0" ry="0" width="107" height="84" />
<rect x="198" y="451" rx="0" ry="0" width="149" height="28" />
<rect x="255" y="490" rx="0" ry="0" width="85" height="16" />
<rect x="203" y="518" rx="0" ry="0" width="128" height="15" />
<rect x="206" y="489" rx="0" ry="0" width="31" height="19" />
<rect x="34" y="559" rx="0" ry="0" width="107" height="84" />
<rect x="200" y="565" rx="0" ry="0" width="149" height="28" />
<rect x="257" y="604" rx="0" ry="0" width="85" height="16" />
<rect x="205" y="632" rx="0" ry="0" width="128" height="15" />
<rect x="208" y="603" rx="0" ry="0" width="31" height="19" />
<rect x="38" y="674" rx="0" ry="0" width="107" height="84" />
<rect x="261" y="719" rx="0" ry="0" width="85" height="16" />
<rect x="209" y="747" rx="0" ry="0" width="128" height="15" />
<rect x="212" y="718" rx="0" ry="0" width="31" height="19" />
</content-loader>
</template>
<script>
import { ContentLoader } from 'vue-content-loader';
export default {
components: {
ContentLoader
}
};
</script>