环境搭建事先准备
本文只注重前端的搭建,后端只提及部分
本框架是基于vue-cli3搭建的,首先你需要安装vue-cli脚手架。
新手上路,有什么写错的请指点
- 查看你的版本,cmd命令
vue -V
- 创建你的项目(admin为项目名称)
vue create admin
然后安装你的各种负载,这里不进行详细说明
我的package.json可以进行参考
"dependencies": {
"axios": "^0.19.0",
"babel-plugin-import": "^1.12.2",
"core-js": "^3.3.2",
"enquire.js": "^2.1.6",
"less": "^3.10.3",
"vue": "^2.6.10",
"vue-apexcharts": "^1.5.1",
"vue-router": "^3.1.3",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0",
"@vue/cli-plugin-eslint": "^4.0.0",
"@vue/cli-service": "^4.0.0",
"@vue/eslint-config-standard": "^4.0.0",
"babel-eslint": "^10.0.3",
"babel-plugin-component": "^1.1.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"less-loader": "^5.0.0",
"lint-staged": "^9.4.2",
"stylus": "^0.54.7",
"stylus-loader": "^3.0.2",
"svg-sprite-loader": "^4.1.6",
"vue-template-compiler": "^2.6.10"
},
- 配置vue.config.js
在vue-cli3以后,目录结构十分精简,那么我们的配置写在哪里?
官方文档有说明,在项目根目录下面新建vue.config.js文件
以下我的配置,可以结合官网文档参考一下
const path = require('path') // 引入path模块
function resolve (dir) {
return path.join(__dirname, dir) // path.join(__dirname)设置绝对路径
}
module.exports = {
publicPath: '/', // 基本路径
outputDir: 'dist', // 输出文件目录
lintOnSave: true, // eslint-loader 是否在保存的时候检查
productionSourceMap: true, // 生产环境是否生成 sourceMap 文件
// css相关配置
css: {
extract: true, // 是否使用css分离插件 ExtractTextPlugin
sourceMap: false, // 开启 CSS source maps
modules: false,
loaderOptions: {
less: {
modifyVars: {
'primary-color': '#1DA57A',
'link-color': '#1DA57A',
'border-radius-base': '2px'
},
javascriptEnabled: true
}
}
},
chainWebpack: config => {
// 配置路径别名
config.resolve.alias
.set('@', resolve('src'))
.set('_c', resolve('src/components'))
.set('_v', resolve('src/views'))
.set('_u', resolve('src/utils'))
.set('_m', resolve('src/common/mixin'))
.set('_api', resolve('src/api'))
config.module.rules.delete('svg') // 配置svg-sprite-loader,没有此需求请注释
config.module
.rule('svg-smart')
.test(/\.svg$/)
.include
.add(resolve('src/assets/svgs'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
},
// webpack-dev-server 相关配置
devServer: {
open: false, // 是否在构建完成后打开默认浏览器
host: '127.0.0.1', // 监听地址
port: 8999, // 端口
https: false, // 是否有必须通过https的服务
hotOnly: false,
proxy: {
'/api': {
target: 'url', //代理
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
},
'/foo': {
target: '<other_url>'
}
},
before: app => {}
},
// 第三方插件配置
pluginOptions: {}
}
以上便是当前目录结构
- 路由配置
路由使用的是动态路由,分为两部分:不需要鉴权的和需要鉴权的
以下便是基础路由(不需要鉴权的):
import Vue from 'vue'
import Router from 'vue-router'
// 全局Router异常处理
const originalPush = Router.prototype.push
Router.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => { if (typeof err !== 'undefined')console.log(err) })
}
Vue.use(Router)
let constRouter = [
{
path: '/login',
name: '登录页',
component: LoginView // 需要自行引入
},
{ // 主界面,基础路由中先不指定组件
path: '/',
name: '主界面',
redirect: '/home'
},
{
path: '*',
name: '404',
component: 404// 需要自行引入
},
]
let router = new Router({
// mode: 'history',
routes: constRouter
})
export default router
需要鉴权的路由,在登录时候,后台拼接返回路由
我们需要用到router.addRoutes来添加动态路由。
在登陆系统的时候,如果登录成功,然后返回一个SESSIONID或者JWT作为身份鉴权标识或者没有???(取决于你的后台,这里不过多叙述),然后重定向路由
login({ // 登录请求,后面会讲到
username: name,
password: password
}).then((r) => {
this.$router.push('/') //重定向路由
}).catch((e) => {
console.error(e)
})
- 路由拦截配置
这部分借鉴github上面一个大佬的,找半天地址没找到
import router from './../router'
import localStore from '_u/localstorage' // 存到浏览器
import request from '_u/request' // axios请求
const whiteList = ['/login'] // 白名单
let asyncRouter // 新增路由
// 导航守卫,渲染动态路由
router.beforeEach((to, from, next) => {
if (whiteList.indexOf(to.path) !== -1) {
next()
}
let token = localStore.get('USER_TOKEN')
let user = localStore.get('USER')
let userRouter = get('USER_ROUTER')
if (token.length && user) {
if (!asyncRouter) {
if (!userRouter) {
request.get(`menu/${user.username}`).then((res) => {
asyncRouter = res.data
save('USER_ROUTER', asyncRouter)
go(to, next)
}).catch(err => { console.error(err) })
} else {
asyncRouter = userRouter
go(to, next)
}
} else {
next()
}
} else {
next('/login')
}
})
function go (to, next) {
asyncRouter = filterAsyncRouter(asyncRouter)
console.log(asyncRouter)
router.addRoutes(asyncRouter)
next({ ...to, replace: true })
}
function save (name, data) {
localStorage.setItem(name, JSON.stringify(data))
}
function get (name) {
return JSON.parse(localStorage.getItem(name))
}
function filterAsyncRouter (routes) {
return routes.filter((route) => {
let component = route.component
if (component) {
switch (route.component) {
case 'MenuView':
route.component = MenuView // 表示菜单页面
break
case 'PageView':
route.component = PageView // 表示页面
break
case 'EmptyPageView':
route.component = EmptyPageView //表示空白页面
break
case 'HomePageView':
route.component = HomePageView //表示主界面
break
default:
route.component = view(component) // 其他均为组件
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
}
})
}
function view (path) {
return function (resolve) {
import(`@/views/${path}.vue`).then(mod => {
resolve(mod)
})
}
}
刚刚登录请求重定向会被路由拦截器给拦截,判断用户是否有登录,没有登录就会请求后台获取权限(动态路由),然后存储在本地。接下来动态添加路由,这就完成了路由的动态添加。
router.addRoutes(asyncRouter) //上面有
- axios的配置
在上面其实已经用到了请求,下面讲解如何配置axios。
import axios from 'axios'
import { message, Modal, notification } from 'ant-design-vue' // 前端框架
import moment from 'moment'
import store from '../store' // vuex
import localstore from '_u/localstorage' // 本地存储
moment.locale('zh-cn') // 语言
// 统一配置
let REQUEST= axios.create({
baseURL: 'http://127.0.0.1:9527/', // 后台地址,如果启用api代理,在vue.config.js要进行修改
responseType: 'json',
validateStatus (status) {
// 200 外的状态码都认定为失败
return status === 200
}
})
// 拦截请求
REQUEST.interceptors.request.use((config) => {
if (config.url !== 'login') {
let expireTime = store.state.account.expireTime
let now = moment().format('YYYYMMDDHHmmss')
if (now - expireTime >= -10) {
Modal.error({
title: '登录已过期',
content: '很抱歉,登录已过期,请重新登录',
okText: '重新登录',
mask: false,
onOk: () => {
return new Promise((resolve, reject) => {
localstore.clear()
location.reload()
}).catch(function (reason) {
console.log('catch:', reason)
})
}
})
}
}
if (store.state.account.token) {
config.headers.Authentication = store.state.account.token
}
return config
}, (error) => {
return Promise.reject(error)
})
// 拦截响应
REQUEST.interceptors.response.use((config) => {
return config
}, (error) => {
if (error.response) {
let errorMessage = error.response.data === null ? '系统内部异常,请联系网站管理员' : error.response.data.message
switch (error.response.status) {
case 404:
notification.error({
message: '系统提示',
description: '很抱歉,资源未找到',
duration: 4
})
break
case 403:
case 401:
notification.warn({
message: '系统提示',
description: '没有相应权限或者登录已失效',
duration: 4
})
localstore.clear()
break
default:
notification.error({
message: '系统提示',
description: errorMessage,
duration: 4
})
break
}
}
return Promise.resolve(error)
})
export default request
然后请求格式:
import request from '_u/request' // 上面哪个axios实例
export function login (data) {
return request({
method: 'post',
url: '/login',
data: data
})
}
再补充一下目录结构
其他(按钮权限)
- 按钮权限列表
如果你想给不同用户展示不同的按钮的时候(数据权限),你会用到这个。
哪个你后台不光需要返回动态路由,还需要返回按钮权限数组列表
这样的一个东西,然后存储再vuex(刷新会丢失),还要存储在本地
-
自定义指令
官方文档
install.js 全局注册,注意要在main.js里面引用
import Vue from 'vue'
import { hasPermission, hasNoPermission} from '_u/permissionDirect'
const Plugins = [
hasPermission,
hasNoPermission,
hasAnyPermission,
hasRole,
hasAnyRole
]
Plugins.map((plugin) => {
Vue.use(plugin)
})
export default Vue
// 必须包含列出的所有权限,元素才显示
export const hasPermission = {
install (Vue) {
Vue.directive('hasPermission', {
bind (el, binding, vnode) {
let permissions = vnode.context.$store.state.account.permissions
let value = binding.value
let flag = true
for (let v of value) {
if (!permissions.includes(v)) {
flag = false
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = 'none'
} else {
el.parentNode.removeChild(el)
}
}
}
})
}
}
// 当不包含列出的权限时,渲染该元素
export const hasNoPermission = {
install (Vue) {
Vue.directive('hasNoPermission', {
bind (el, binding, vnode) {
let permissions = vnode.context.$store.state.account.permissions
let value = binding.value
let flag = true
for (let v of value) {
if (permissions.includes(v)) {
flag = false
}
}
if (!flag) {
if (!el.parentNode) {
el.style.display = 'none'
} else {
el.parentNode.removeChild(el)
}
}
}
})
}
}
看着很多,就是一句话,判断有没有这个权限,没有就不渲染这个按钮
使用就是下面这个样子
<a-button @click="batchDelete" v-hasPermission="['user:delete']">删除</a-button>