目录结构
├──public│ └── logo.png # LOGO|└── index.html # Vue 入口模板├── src│ ├── api #Apiajax 等│ ├── assets # 本地静态资源│ ├── config # 项目基础配置,包含路由,全局设置│ ├── components # 业务通用组件│ ├── core # 项目引导,全局配置初始化,依赖包引入等│ ├── router # Vue-Router│ ├── store # Vuex│ ├── utils # 工具库│ ├── locales # 国际化资源│ ├── views # 业务页面入口和常用模板│ ├── App.vue # Vue 模板入口│ └── main.js # Vue 入口 JS│ └── permission.js # 路由守卫(路由权限控制)├── tests # 测试工具├── README.md└── package.json
路由和菜单
基本结构
路由和菜单是组织起一个应用的关键骨架,pro 中的路由为了方便管理,使用了中心化的方式,在 ==router.config.js== 统一配置和管理。
路由管理 通过约定的语法根据在==router.config.js==中配置路由。
菜单生成 根据路由配置来生成菜单。菜单项名称,嵌套路径与路由高度耦合。
面包屑 组件 ==PageHeader== 中内置的面包屑也可由脚手架提供的配置信息自动生成。
路由
目前脚手架中所有的路由都通过 ==router.config.js== 来统一管理,在 ==vue-router== 的配置中我们增加了一些参数,如 ==hideChildrenInMenu==,==meta.title==,==meta.icon==,==meta.permission==,来辅助生成菜单。其中:
hideChildrenInMenu 用于隐藏不需要在菜单中展示的子路由。用法可以查看 分步表单 的配置。
hidden 可以在菜单中不展示这个路由,包括子路由。效果可以查看 other 下的路由配置。
meta.title 和 meta.icon分别代表生成菜单项的文本和图标。
meta.permission 用来配置这个路由的权限,如果配置了将会验证当前用户的权限,并决定是否展示 *(默认情况下)。
meta.hidden 可以强制子菜单不显示在菜单上(和父级 hideChildrenInMenu 配合)
meta.hiddenHeaderContent 可以强制当前页面不显示 PageHeader 组件中的页面带的 面包屑和页面标题栏
路由配置项
/**
* 路由配置说明:
* 建议:sider menu 请不要超过三级菜单,若超过三级菜单,则应该设计为顶部主菜单 配合左侧次级菜单
*
**/{redirect:noredirect,//重定向name:'router-name',//路由名称hidden:true,//可以在菜单中不展示这个路由,包括子路由。效果可以查看 other 下的路由配置。meta:{title:'title',//菜单项名称icon:'a-icon',//菜单项图标keepAlive:true,//缓存页面permission:[string]//用来配置这个路由的权限,如果配置了将会验证当前用户的权限,并决定是否展示 *(默认情况下)hiddenHeaderContent:true,//可以强制当前页面不显示 PageHeader 组件中的页面带的 面包屑和页面标题栏}}
具体请参考https://pro.loacg.com/docs/router-and-nav
菜单
菜单根据 ==router.config.js== 生成,具体逻辑在 ==src/store/modules/permission.js== 中的 ==actions.GenerateRoutes== 方法实现。
Ant Design Pro 的布局
在 Ant Design Pro 中,我们抽离了使用过程中的通用布局,都放在 ==/components/layouts== 目录中,分别为:
BasicLayout:基础页面布局,包含了头部导航,侧边栏和通知栏:
UserLayout:抽离出用于登陆注册页面的通用布局
PageView:基础布局,包含了面包屑,和中间内容区 (slot)
RouterView:空布局,专门为了二级菜单内容区自定义
BlankLayout:空白的布局
定义全局样式
/* 定义全局样式 */:global(.text){font-size:16px;}/* 定义多个全局样式 */:global{.footer{color:#ccc;}.sider{background:#ebebeb;}}//覆盖组件样式// 使用 css 时可以用 >>> 进行样式穿透.test-wrapper>>>.ant-select{font-size:16px;}// 使用 scss, less 时,可以用 /deep/ 进行样式穿透.test-wrapper/deep/.ant-select{font-size:16px;}// less CSS modules 时亦可用 :global 进行覆盖.test-wrapper{:global{.ant-select{font-size:16px;}}}
与服务器交互
在 Ant Design Pro 中,一个完整的前端 UI 交互到服务端处理流程是这样的:
UI 组件交互操作;
调用统一管理的 api service 请求函数;
使用封装的 request.js 发送请求;
获取服务端返回;
更新 data。
从上面的流程可以看出,为了方便管理维护,统一的请求处理都放在 @/src/api 文件夹中,并且一般按照 model 纬度进行拆分文件,如:
api/ user.js permission.js goods.js ...
其中,==@/src/utils/request.js== 是基于 ==axios== 的封装,便于统一处理 ==POST==,==GET== 等请求参数,请求头,以及错误提示信息等。具体可以参看 ==request.js==。 它封装了全局 request 拦截器、response 拦截器、统一的错误处理、baseURL 设置等。
例如在 api 中的一个请求用户信息的例子:
// api/user.jsimport{axios}fromm'@/utils/request'constapi={info:'/user',list:'/users'}// 根据用户 id 获取用户信息exportfunctiongetUser(id){returnaxios({url:`${api.user}/${id}`,method:'get'})}// 增加用户exportfunctionaddUser(parameter){returnaxios({url:api.user,method:'post',data:parameter})}// 更新用户 // or (id, parameter)exportfunctionupdateUser(parameter){returnaxios({url:`${api.user}/${parameter.id}`,// or `${api.user}/${id}`method:'put',data:parameter})}// 删除用户exportfunctiondeleteUser(id){returnaxios({url:`${api.user}/${id}`,method:'delete',data:parameter})}// 获取用户列表 parameter: { pageSize: 10, pageNo: 1 }exportfunctiongetUsers(parameter){returnaxios({url:api.list,method:'get',params:parameter})}
<template><div><a-button@click="queryUser"></a-button><a-table:dataSource="list"></a-table></div></template><script>import { getUser, getUsers } from '@/api/user'export default { data () { return { id: 0, queryParam: { pageSize: 10, pageNo: 1, username: '' }, info: {}, list: [] } }, methods: { queryUser () { const { $message } = this getUser(this.id).then(res => { this.info = res.data }).catch(err => { $message.error(`load user err: ${err.message}`) }) }, queryUsers () { getUsers(this.queryParam).then(res => { this.list = res }) } }}</script>
***获取裁剪后的图片*/cropImage(){this.form.cropimg=this.$refs.cropper.getCroppedCanvas().toDataURL();},/**
* 确认裁剪
*/sureCrop(){this.dialogVisible=false},/**
* 上传裁剪后的图片到服务器
*/upCropImg(){//判断是否是新增还是编辑if(this.$route.query.id&&this.$route.query.id!=''){//如果是编辑的就直接提交this.onSubmit()}else{//否则先上传裁剪图片,将64位图片转换为二进制数据流varformdata1=newFormData();// 创建form对象formdata1.append('file',convertBase64UrlToBlob(this.form.cropimg),'aaa.png');//this.$ajax.post(this.$api+"/upload/singleUploadImg",formdata1,{headers:{'Content-Type':'multipart/form-data'}}).then(response=>{if(response.data.msg=="success"&&response.data.code==1){this.form.imgUrl=response.data.data.imgUrlthis.onSubmit()}else{console.log(response)this.$message.error(response.data.msg);}}).catch(function(error){console.log(error);});}},
引入外部模块
$ npm install'组件名字'--save
使用
//全局引入importVuefrom'vue'importVueQuillEditorfrom'vue-quill-editor'// require stylesimport'quill/dist/quill.core.css'import'quill/dist/quill.snow.css'import'quill/dist/quill.bubble.css'Vue.use(VueQuillEditor,/* { default global options } */)
<template><div><quill-editorref="myTextEditor"v-model="content":config="editorOption"@blur="onEditorBlur($event)"@focus="onEditorFocus($event)"@ready="onEditorReady($event)"></quill-editor></div></template><script>//按需加载import 'quill/dist/quill.core.css'import 'quill/dist/quill.snow.css'import 'quill/dist/quill.bubble.css'import { quillEditor } from 'vue-quill-editor'export default { components: { quillEditor }, data () { return { content: '<h2>I am Example</h2>', editorOption: { // something config } } }, // 如果需要手动控制数据同步,父组件需要显式地处理changed事件 methods: { onEditorBlur(editor) { console.log('editor blur!', editor) }, onEditorFocus(editor) { console.log('editor focus!', editor) }, onEditorReady(editor) { console.log('editor ready!', editor) }, onEditorChange({ editor, html, text }) { // console.log('editor change!', editor, html, text) this.content = html } }, // 如果你需要得到当前的editor对象来做一些事情,你可以像下面这样定义一个方法属性来获取当前的editor对象,实际上这里的$refs对应的是当前组件内所有关联了ref属性的组件元素对象 computed: { editor() { return this.$refs.myTextEditor.quillEditor } }, mounted() { // you can use current editor object to do something(editor methods) console.log('this is my editor', this.editor) // this.editor to do something... }}</script>
引入业务图标
参考:https://pro.loacg.com/docs/biz-icon、
国际化
参考:https://pro.loacg.com/docs/i18n
权限管理
参考:https://pro.loacg.com/docs/authority-management
自定义使用规则
修改网站icon的文件地址在 ==public文件夹==中把logo.png换成自定义的,也可在==public/index.html==自定义
<!DOCTYPE html><htmllang="zh-cmn-Hans"><head><metacharset="utf-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width,initial-scale=1.0"><linkrel="icon"href="<%= BASE_URL %>logo.png"><title>共享云店</title><style>#loading-mask{position:fixed;left:0;top:0;height:100%;width:100%;background:#fff;user-select:none;z-index:9999;overflow:hidden}.loading-wrapper{position:absolute;top:50%;left:50%;transform:translate(-50%,-100%)}.loading-dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:64px;width:64px;height:64px;box-sizing:border-box}.loading-dot i{width:22px;height:22px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.loading-dot i:nth-child(1){top:0;left:0}.loading-dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.loading-dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.loading-dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><divid="app"><divid="loading-mask"><divclass="loading-wrapper"><spanclass="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span></div></div></div><!-- built files will be auto injected --></body></html>
-更换logo在==src\components\tools\Logo.vue==中更换
<template><divclass="logo"><router-link:to="{name:'dashboard'}"><LogoSvgalt="logo"/>//这是logo<h1v-if="showTitle">{{ title }}</h1>//这是网站标题</router-link></div></template><script>import LogoSvg from '@/assets/logo.svg?inline';export default { name: 'Logo', components: { LogoSvg }, props: { title: { type: String, default: 'Admin For Ok', //网站默认标题 required: false }, showTitle: { //是否显示网站标题,默认不显示 type: Boolean, default: true, required: false } }}</script>
pro权限管理和路由控制思路分析(粗略分析)
主要是通过三个文件实现,==src\mock\services\user.js==文件通过登陆的角色获取对应的鉴权规则,具体可查看该文件下的源码
==src\config\router.config.js==文件为路由配置文件,可增加路由取消路由等,变量asyncRouterMap为主要路由数组集合,可配置鉴权权限,变量constantRouterMap为基础路由,不参与鉴权
==src\permission.js==文件为动态配置路由的主要逻辑,代码如下
importVuefrom'vue'importrouterfrom'./router'importstorefrom'./store'importNProgressfrom'nprogress'// progress barimport'nprogress/nprogress.css'// progress bar styleimportnotificationfrom'ant-design-vue/es/notification'import{setDocumentTitle,domTitle}from'@/utils/domUtil'import{ACCESS_TOKEN}from'@/store/mutation-types'NProgress.configure({showSpinner:false})// NProgress ConfigurationconstwhiteList=['login','register','registerResult']// no redirect whitelist配置白名单router.beforeEach((to,from,next)=>{NProgress.start()// start progress bar//生成动态网站标题to.meta&&(typeofto.meta.title!=='undefined'&&setDocumentTitle(`${to.meta.title} - ${domTitle}`))if(Vue.ls.get(ACCESS_TOKEN)){/* has token 如果有token并且是从登录页来的就直接跳到工作空间*/if(to.path==='/user/login'){next({path:'/dashboard/workplace'})NProgress.done()}else{//否则检测是不是没有检测到规则,请求获取用户信息,获取用户权限if(store.getters.roles.length===0){//请求mock模拟数据获取用户权限store.dispatch('GetInfo').then(res=>{constroles=res.result&&res.result.role//调用src\store\modules\permission.js里面的GenerateRoutes方法,处理数据store.dispatch('GenerateRoutes',{roles}).then(()=>{// 根据roles权限生成可访问的路由表// 动态添加可访问路由表router.addRoutes(store.getters.addRouters)constredirect=decodeURIComponent(from.query.redirect||to.path)if(to.path===redirect){// hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history recordnext({...to,replace:true})}else{// 跳转到目的路由next({path:redirect})}})}).catch(()=>{notification.error({message:'错误',description:'请求用户信息失败,请重试'})store.dispatch('Logout').then(()=>{next({path:'/user/login',query:{redirect:to.fullPath}})})})}else{next()}}}else{if(whiteList.includes(to.name)){// 在免登录白名单,直接进入next()}else{next({path:'/user/login',query:{redirect:to.fullPath}})NProgress.done()// if current page is login will not trigger afterEach hook, so manually handle it}}})router.afterEach(()=>{NProgress.done()// finish progress bar})
==src\store\modules\permission.js==文件为路由数据的详细处理逻辑,配合src\permission.js文件使用,代码如下:
import{asyncRouterMap,constantRouterMap}from'@/config/router.config'/**
* 过滤账户是否拥有某一个权限,并将菜单从加载列表移除
*
* @param permission
* @param route
* @returns {boolean}
*/functionhasPermission(permission,route){if(route.meta&&route.meta.permission){letflag=falsefor(leti=0,len=permission.length;i<len;i++){flag=route.meta.permission.includes(permission[i])if(flag){returntrue}}returnfalse}returntrue}/**
* 单账户多角色时,使用该方法可过滤角色不存在的菜单
*
* @param roles
* @param route
* @returns {*}
*/// eslint-disable-next-linefunctionhasRole(roles,route){if(route.meta&&route.meta.roles){returnroute.meta.roles.includes(roles.id)}else{returntrue}}functionfilterAsyncRouter(routerMap,roles){constaccessedRouters=routerMap.filter(route=>{if(hasPermission(roles.permissionList,route)){if(route.children&&route.children.length){route.children=filterAsyncRouter(route.children,roles)}returntrue}returnfalse})returnaccessedRouters}constpermission={state:{routers:constantRouterMap,addRouters:[]},mutations:{SET_ROUTERS:(state,routers)=>{state.addRouters=routers state.routers=constantRouterMap.concat(routers)}},actions:{GenerateRoutes({commit},data){returnnewPromise(resolve=>{const{roles}=dataconstaccessedRouters=filterAsyncRouter(asyncRouterMap,roles)commit('SET_ROUTERS',accessedRouters)resolve()})}}}exportdefaultpermission
跨域请求设置
在==vue.config.js==文件中修改
// 配置跨域devServer:{// development server port 8000// port: 8000,proxy:{'/apis':{// target: 'https://mock.ihx.me/mock/5baf3052f7da7e07e04a5116/antd-pro',target:'http://192.168.1.73:8092/okcloud/',// target: 'http://39.107.78.120:8083/okcloud/',ws:false,changeOrigin:true,//是否允许跨域pathRewrite:{'^/apis':''}}}
axios拦截器
在文件==src\utils\request.js==中设置
// request interceptorservice.interceptors.request.use(config=>{consttoken=Vue.ls.get(ACCESS_TOKEN)if(token){config.headers['okcloud_token']=token// 让每个请求携带自定义 token 请根据实际情况自行修改}returnconfig},err)// response interceptorservice.interceptors.response.use((response)=>{if(response.data.code===10000){notification.warning({message:'提示',description:response.data.message})}else{returnresponse.data}},err)
自定义角色的菜单权限
在==src\main.js==文件中注释掉"// import './permission' // permission control 权限控制"
自定义路由权限文件==src\routeGuard.js==,代码如下
importVuefrom'vue'importrouterfrom'./router'// import store from './store'importNProgressfrom'nprogress'// progress barimport'nprogress/nprogress.css'// progress bar styleimport{setDocumentTitle,domTitle}from'@/utils/domUtil'import{ACCESS_TOKEN}from'@/store/mutation-types'NProgress.configure({showSpinner:false})// NProgress Configurationrouter.beforeEach((to,from,next)=>{NProgress.start()// start progress barto.meta&&(typeofto.meta.title!=='undefined'&&setDocumentTitle(`${to.meta.title} - ${domTitle}`))if(to.path==='/user/login'&&Vue.ls.get(ACCESS_TOKEN)){next({path:'/dashboard/workplace'})NProgress.done()}elseif(to.path!=='/user/login'&&!Vue.ls.get(ACCESS_TOKEN)){next({path:'/user/login'})NProgress.done()}else{next()NProgress.done()}})router.afterEach(()=>{NProgress.done()// finish progress bar})
在==main.js==中引入import './routeGuard'
对==src\components\Menu\menu.js==文件进行自定义菜单改造
在==src\store\modules\app.js==文件中store加入menu,在actions中进行获取菜单的异步操作,获取菜单信息,然后进行全局变量动态获取
在==src\layouts\BasicLayout.vue==中进行全局变量的引用
...mapState({// 动态主路由menus:state=>state.app.menu}),
动态方法的引用
...mapActions(['setSidebar', 'setMenu']),
调用获取动态方法
this.setMenu()
作者:27亿光年中的小小尘埃
链接:https://www.jianshu.com/p/2c004a98ebf1
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。