动态路由

This commit is contained in:
aoli.qu 2021-09-01 10:28:24 +08:00
parent ff40c9afa0
commit a665afac52
12 changed files with 167 additions and 308 deletions

View File

@ -9,7 +9,6 @@
<script>
import { domTitle, setDocumentTitle } from '@/utils/domUtil'
import { i18nRender } from '@/locales'
import { mapActions } from 'vuex'
export default {
data () {
@ -23,12 +22,6 @@ export default {
title && (setDocumentTitle(`${i18nRender(title)} - ${domTitle}`))
return this.$i18n.getLocaleMessage(this.$store.getters.lang).antLocale
}
},
methods: {
...mapActions(['InitAccessToken'])
},
mounted () {
this.InitAccessToken()
}
}
</script>

View File

@ -1,80 +1,40 @@
import request from '@/utils/request'
const userApi = {
Login: '/auth/login',
Logout: '/auth/logout',
ForgePassword: '/auth/forge-password',
Register: '/auth/register',
twoStepCode: '/auth/2step-code',
SendSms: '/account/sms',
SendSmsErr: '/account/sms_err',
// get my info
UserInfo: '/user/info',
UserMenu: '/user/nav'
login: '/login',
logout: '/logout',
getLoginUser: '/getLoginUser'
}
/**
* login func
* parameter: {
* username: '',
* password: '',
* remember_me: true,
* captcha: '12345'
* }
* @param parameter
* @returns {*}
*/
export function login (parameter) {
return request({
url: userApi.Login,
url: userApi.login,
method: 'post',
data: parameter
})
}
export function getSmsCaptcha (parameter) {
export function getLoginUser () {
return request({
url: userApi.SendSms,
url: userApi.getLoginUser,
method: 'post',
data: parameter
})
}
export function getInfo () {
return request({
url: userApi.UserInfo,
method: 'get',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
export function getCurrentUserNav () {
return request({
url: userApi.UserMenu,
method: 'get'
})
}
export function logout () {
return request({
url: userApi.Logout,
url: userApi.logout,
method: 'post',
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
}
/**
* get user 2step code open?
* @param parameter {*}
*/
export function get2step (parameter) {
return request({
url: userApi.twoStepCode,
method: 'post',
data: parameter
export function getSmsCaptcha (parameter) {
return axios({
url: '/getSmsCaptcha',
method: 'get',
params: parameter
})
}

View File

@ -11,11 +11,10 @@
* storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage)
*
*/
export default {
navTheme: 'dark', // theme for nav menu
navTheme: 'light', // theme for nav menu
primaryColor: '#1890ff', // primary color of ant design
layout: 'topmenu', // nav menu position: `sidemenu` or `topmenu`
layout: 'sidemenu', // nav menu position: `sidemenu` or `topmenu`
contentWidth: 'Fluid', // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
fixedHeader: false, // sticky header
fixSiderbar: false, // sticky siderbar
@ -23,7 +22,7 @@ export default {
menu: {
locale: true
},
title: '系统后台',
title: '安全培训平台',
pwa: false,
iconfontUrl: '',
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true'

View File

@ -11,11 +11,9 @@
:i18nRender="i18nRender"
v-bind="settings"
>
<!-- <setting-drawer :settings="settings" @change="handleSettingChange" /> -->
<template v-slot:rightContentRender>
<right-content :top-menu="settings.layout === 'topmenu'" :is-mobile="isMobile" :theme="settings.theme" />
</template>
<!-- <multi-tab></multi-tab> -->
<template v-slot:footerRender>
<global-footer />
</template>
@ -24,25 +22,20 @@
</template>
<script>
import { updateTheme } from '@ant-design-vue/pro-layout'
import { i18nRender } from '@/locales'
import { mapState } from 'vuex'
import { SIDEBAR_TYPE, TOGGLE_MOBILE_TYPE } from '@/store/mutation-types'
import { asyncRouterMap } from '@/config/router.config.js'
import defaultSettings from '@/config/defaultSettings'
import RightContent from '@/components/GlobalHeader/RightContent'
import GlobalFooter from '@/components/GlobalFooter'
import MultiTab from '@/components/MultiTab'
import LogoSvg from '../assets/logo.svg?inline'
export default {
name: 'BasicLayout',
components: {
// SettingDrawer,
RightContent,
GlobalFooter,
MultiTab
GlobalFooter
},
data () {
return {
@ -63,13 +56,11 @@ export default {
fixedHeader: defaultSettings.fixedHeader,
fixSiderbar: defaultSettings.fixSiderbar,
colorWeak: defaultSettings.colorWeak,
hideHintAlert: false,
hideCopyButton: false
},
//
query: {},
//
isMobile: false
}
@ -81,16 +72,15 @@ export default {
})
},
created () {
// const routes = this.mainMenu.find(item => item.path === '/')
// this.menus = (routes && routes.children) || []
this.menus = asyncRouterMap.find((item) => item.path === '/').children
//
this.$watch('collapsed', () => {
this.$store.commit(SIDEBAR_TYPE, this.collapsed)
})
this.$watch('isMobile', () => {
this.$store.commit(TOGGLE_MOBILE_TYPE, this.isMobile)
})
const routes = this.mainMenu.find(item => item.path === '/')
this.menus = (routes && routes.children) || []
//
this.$watch('collapsed', () => {
this.$store.commit(SIDEBAR_TYPE, this.collapsed)
})
this.$watch('isMobile', () => {
this.$store.commit(TOGGLE_MOBILE_TYPE, this.isMobile)
})
},
mounted () {
const userAgent = navigator.userAgent
@ -102,12 +92,6 @@ export default {
}, 16)
})
}
// first update color
// TIPS: THEME COLOR HANDLER!! PLEASE CHECK THAT!!
if (process.env.NODE_ENV !== 'production' || process.env.VUE_APP_PREVIEW === 'true') {
updateTheme(this.settings.primaryColor)
}
},
methods: {
i18nRender,
@ -127,23 +111,6 @@ export default {
handleCollapse (val) {
this.collapsed = val
},
handleSettingChange ({ type, value }) {
console.log('type', type, value)
type && (this.settings[type] = value)
switch (type) {
case 'contentWidth':
this.settings[type] = value === 'Fixed'
break
case 'layout':
if (value === 'sidemenu') {
this.settings.contentWidth = false
} else {
this.settings.fixSiderbar = false
this.settings.contentWidth = true
}
break
}
},
logoRender () {
return <LogoSvg />
}

View File

@ -15,7 +15,7 @@ const loginRoutePath = '/user/login'
const defaultRoutePath = '/dashboard/workplace'
router.beforeEach((to, from, next) => {
console.log("判断路由")
debugger
NProgress.start() // start progress bar
to.meta && (typeof to.meta.title !== 'undefined' && setDocumentTitle(`${i18nRender(to.meta.title)} - ${domTitle}`))
/* has token */
@ -24,16 +24,30 @@ router.beforeEach((to, from, next) => {
next({ path: defaultRoutePath })
NProgress.done()
} else {
// check login user.roles is null
if (store.getters.roles.length === 0) {
// request login userInfo
console.log('逻辑开始1 => src/permission.js')
store
.dispatch('GetInfo')
.then(res => {
const roles = res.result && res.result.role
console.log('流程3-获取个人信息返回 => src/permission.js ')
if (res.menus.length < 1) {
Modal.error({
title: '提示:',
content: '无菜单权限,请联系管理员',
okText: '确定',
onOk: () => {
store.dispatch('Logout').then(() => {
window.location.reload()
})
}
})
return
}
const antDesignMenus = res.menus
// generate dynamic router
store.dispatch('GenerateRoutes', { roles }).then(() => {
// 根据roles权限生成可访问的路由表
console.log('流程4-从后端获取权限开始')
store.dispatch('GenerateRoutes', { antDesignMenus }).then(() => {
// 根据菜单权限生成可访问的路由表
// 动态添加可访问路由表
router.addRoutes(store.getters.addRouters)
// 请求带有 redirect 重定向时,登录自动重定向到该地址
@ -48,11 +62,6 @@ router.beforeEach((to, from, next) => {
})
})
.catch(() => {
notification.error({
message: '错误',
description: '请求用户信息失败,请重试'
})
// 失败时,获取用户信息失败时,调用登出,来清空历史保留信息
store.dispatch('Logout').then(() => {
next({ path: loginRoutePath, query: { redirect: to.fullPath } })
})

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line
import * as loginService from '@/api/login'
// eslint-disable-next-line
import { BasicLayout, BlankLayout, PageView, RouteView } from '@/layouts'
@ -16,39 +15,6 @@ const constantRouterComponents = {
// 你需要动态引入的页面组件
'Workplace': () => import('@/views/dashboard/Workplace'),
// form
'BasicForm': () => import('@/views/form/basicForm/Index'),
'StepForm': () => import('@/views/form/stepForm/StepForm'),
'AdvanceForm': () => import('@/views/form/advancedForm/AdvancedForm'),
// list
'TableList': () => import('@/views/list/TableList'),
'StandardList': () => import('@/views/list/StandardList'),
'CardList': () => import('@/views/list/CardList'),
'SearchLayout': () => import('@/views/list/search/SearchLayout'),
'SearchArticles': () => import('@/views/list/search/Article'),
'SearchProjects': () => import('@/views/list/search/Projects'),
'SearchApplications': () => import('@/views/list/search/Applications'),
'ProfileBasic': () => import('@/views/profile/basic/Index'),
'ProfileAdvanced': () => import('@/views/profile/advanced/Advanced'),
// result
'ResultSuccess': () => import(/* webpackChunkName: "result" */ '@/views/result/Success'),
'ResultFail': () => import(/* webpackChunkName: "result" */ '@/views/result/Error'),
// exception
'Exception403': () => import(/* webpackChunkName: "fail" */ '@/views/exception/403'),
'Exception404': () => import(/* webpackChunkName: "fail" */ '@/views/exception/404'),
'Exception500': () => import(/* webpackChunkName: "fail" */ '@/views/exception/500'),
// account
'AccountCenter': () => import('@/views/account/center/Index'),
'AccountSettings': () => import('@/views/account/settings/Index'),
'BaseSettings': () => import('@/views/account/settings/BaseSetting'),
'SecuritySettings': () => import('@/views/account/settings/Security'),
'CustomSettings': () => import('@/views/account/settings/Custom'),
'BindingSettings': () => import('@/views/account/settings/Binding'),
'NotificationSettings': () => import('@/views/account/settings/Notification')
}
// 前端未找到页面路由(固定不用改)
@ -58,14 +24,10 @@ const notFoundRouter = {
// 根级菜单
const rootRouter = {
key: '',
path: '/',
name: 'index',
path: '',
component: 'BasicLayout',
redirect: '/dashboard',
meta: {
title: '首页'
},
meta: { title: '首页' },
children: []
}
@ -74,24 +36,26 @@ const rootRouter = {
* @param token
* @returns {Promise<Router>}
*/
export const generatorDynamicRouter = (token) => {
export const generatorDynamicRouter = (data) => {
return new Promise((resolve, reject) => {
loginService.getCurrentUserNav(token).then(res => {
const { result } = res
const menuNav = []
const childrenNav = []
// 后端数据, 根级树数组, 根级 PID
listToTree(result, childrenNav, 0)
rootRouter.children = childrenNav
menuNav.push(rootRouter)
// console.log('menuNav', menuNav)
const routers = generator(menuNav)
routers.push(notFoundRouter)
// console.log('routers', routers)
resolve(routers)
}).catch(err => {
reject(err)
})
console.log('流程5-后端返回的权限菜单generatorDynamicRouter')
console.log(data)
const resNav = data.antDesignMenus
const menuNav = []
const childrenNav = []
// 后端数据, 根级树数组, 根级 PID
listToTree(resNav, childrenNav, 0)
rootRouter.children = childrenNav
if (childrenNav.length > 0) {
rootRouter.redirect = childrenNav[0].path
}
menuNav.push(rootRouter)
const routers = generator(menuNav)
routers.push(notFoundRouter)
resolve(routers)
}).catch(err => {
// reject('加载菜单失败')
return Promise.reject(err)
})
}

View File

@ -1,10 +1,10 @@
import Vue from 'vue'
import Router from 'vue-router'
import { constantRouterMap, asyncRouterMap } from '@/config/router.config'
import { constantRouterMap } from '@/config/router.config'
// hack router push callback
const originalPush = Router.prototype.push
Router.prototype.push = function push(location, onResolve, onReject) {
Router.prototype.push = function push (location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
@ -12,8 +12,7 @@ Router.prototype.push = function push(location, onResolve, onReject) {
Vue.use(Router)
export default new Router({
mode: 'hash', // history
// base: process.env.BASE_URL,
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap.concat(asyncRouterMap)
mode: 'history',
base: process.env.VUE_APP_PUBLIC_PATH,
routes: constantRouterMap
})

View File

@ -18,11 +18,14 @@ const permission = {
actions: {
GenerateRoutes ({ commit }, data) {
return new Promise(resolve => {
const { token } = data
generatorDynamicRouter(token).then(routers => {
console.log('动态生成路由:GenerateRoutes async-router 111')
generatorDynamicRouter(data).then(routers => {
commit('SET_ROUTERS', routers)
resolve()
})
}).catch(err => {
// eslint-disable-next-line no-undef
reject(err)
})
}
}

View File

@ -8,16 +8,16 @@ import { asyncRouterMap, constantRouterMap } from '@/config/router.config'
* @returns {boolean}
*/
function hasPermission (permission, route) {
if (route.meta && route.meta.permission) {
let flag = false
for (let i = 0, len = permission.length; i < len; i++) {
flag = route.meta.permission.includes(permission[i])
if (flag) {
return true
}
}
return false
}
// if (route.meta && route.meta.permission) {
// let flag = false
// for (let i = 0, len = permission.length; i < len; i++) {
// flag = route.meta.permission.includes(permission[i])
// if (flag) {
// return true
// }
// }
// return false
// }
return true
}
@ -64,8 +64,10 @@ const permission = {
actions: {
GenerateRoutes ({ commit }, data) {
return new Promise(resolve => {
console.log('src/store/下的permission.js')
const { roles } = data
const accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
console.log('动态获取到的菜单列表:'+JSON.stringify(accessedRouters))
commit('SET_ROUTERS', accessedRouters)
resolve()
})

View File

@ -1,5 +1,5 @@
import storage from 'store'
import { userLogin, getInfo } from '@/api/security/user'
import { login, getLoginUser, logout } from '@/api/login'
import { ACCESS_TOKEN } from '@/store/mutation-types'
import { welcome } from '@/utils/util'
@ -10,6 +10,7 @@ const user = {
welcome: '',
avatar: '',
roles: [],
buttons: [], // 按钮权限
info: {}
},
@ -29,6 +30,9 @@ const user = {
},
SET_INFO: (state, info) => {
state.info = info
},
SET_BUTTONS: (state, buttons) => {
state.buttons = buttons
}
},
@ -36,7 +40,7 @@ const user = {
// 登录
Login ({ commit }, userInfo) {
return new Promise((resolve, reject) => {
userLogin(userInfo).then(response => {
login(userInfo).then(response => {
storage.set(ACCESS_TOKEN, response.token, 24 * 60 * 60 * 1000)
commit('SET_TOKEN', response.token)
resolve()
@ -49,39 +53,41 @@ const user = {
// 获取用户信息
GetInfo ({ commit }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
const result = response.result
if (result.role && result.role.permissions.length > 0) {
const role = result.role
role.permissions = result.role.permissions
role.permissions.map(per => {
if (per.actionEntitySet != null && per.actionEntitySet.length > 0) {
const action = per.actionEntitySet.map(action => { return action.action })
per.actionList = action
}
})
role.permissionList = role.permissions.map(permission => { return permission.permissionId })
commit('SET_ROLES', result.role)
commit('SET_INFO', result)
getLoginUser().then(response => {
if (response.code === 200) {
const data = response.data
console.log('流程2-获取到个人信息 => user.js')
console.log(data)
commit('SET_ROLES', 1)
commit('SET_BUTTONS', data.permissions)
commit('SET_INFO', data)
commit('SET_NAME', { name: data.userName, welcome: welcome() })
if (data.avatar != null) {
commit('SET_AVATAR', process.env.VUE_APP_API_BASE_URL + '/sysFileInfo/preview?id=' + data.avatar)
}
resolve(data)
} else {
reject(new Error('getInfo: roles must be a non-null array !'))
// eslint-disable-next-line no-undef
reject(new Error(data.message))
}
commit('SET_NAME', { name: result.name, welcome: welcome() })
commit('SET_AVATAR', result.avatar)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 登出
Logout ({ commit, state }) {
return new Promise((resolve) => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
storage.remove(ACCESS_TOKEN)
resolve()
logout(state.token).then(() => {
resolve()
}).catch(() => {
resolve()
}).finally(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_BUTTONS', [])
})
})
}
}

View File

@ -16,11 +16,11 @@
</a-row>
</a-form>
</div>
<div class="table-operator">
<a-button type="primary" icon="plus" @click="$refs.menuForm.add()">新增菜单</a-button>
</div>
<s-table
ref="table"
:rowKey="(record) => record.id"
@ -110,7 +110,8 @@ export default {
],
loadData: parameter => {
return menuList(Object.assign(parameter, this.queryParam)).then((res) => {
return res.data
const menuList = listToTree(res.data, [], rootParentId)
return menuList
})
}
}
@ -133,8 +134,8 @@ export default {
},
methods: {
handleDel (record) {
menuDel({id: record.id, deleteReason: ""}).then((res) => {
if (res.code == 200) {
menuDel({ id: record.id, deleteReason: '' }).then((res) => {
if (res.code === 200) {
this.$message.success('删除成功')
this.$refs.table.refresh()
} else {

View File

@ -1,33 +1,20 @@
<template>
<div class="main">
<a-form id="formLogin" class="user-layout-login" ref="formLogin" :form="form" @submit="handleSubmit">
<a-alert v-if="isLoginError" type="error" showIcon style="margin-bottom: 24px;" message="账户或密码错误18729260811/a123456789 )" />
<a-form-item>
<a-input size="large" type="text" placeholder="账户: 18729260811" v-decorator="[
'mobile',
{rules: [{ required: true, message: '请输入帐户名或邮箱地址' }], validateTrigger: 'change'}
]">
<a-icon slot="prefix" type="user" :style="{ color: 'rgba(0,0,0,.25)' }" />
<a-form-model ref="ruleForm" class="user-layout-login" :model="form" :rules="rules" @submit.prevent="handleSubmit">
<a-form-model-item>
<a-input size="large" v-model="form.username" placeholder="请输入用户名">
<a-icon slot="prefix" type="user" style="color:rgba(0,0,0,.25)" />
</a-input>
</a-form-item>
<a-form-item>
<a-input-password size="large" placeholder="密码: a123456789" v-decorator="[
'password',
{rules: [{ required: true, message: '请输入密码' }], validateTrigger: 'blur'}
]">
<a-icon slot="prefix" type="lock" :style="{ color: 'rgba(0,0,0,.25)' }" />
</a-form-model-item>
<a-form-model-item>
<a-input-password size="large" v-model="form.password" placeholder="请输入密码">
<a-icon slot="prefix" type="lock" style="color:rgba(0,0,0,.25)" />
</a-input-password>
</a-form-item>
<a-form-item>
<a-checkbox v-decorator="['rememberMe', { valuePropName: 'checked' }]">自动登录</a-checkbox>
<router-link :to="{ name: 'recover', params: { user: 'aaa'} }" class="forge-password" style="float: right;">忘记密码</router-link>
</a-form-item>
<a-form-item style="margin-top:24px">
<a-button size="large" type="primary" htmlType="submit" class="login-button" :loading="state.loginBtn" :disabled="state.loginBtn">确定</a-button>
</a-form-item>
</a-form>
</a-form-model-item>
<a-form-model-item>
<a-button block type="primary" size="large" html-type="submit" :loading="loading">登录</a-button>
</a-form-model-item>
</a-form-model>
</div>
</template>
@ -36,53 +23,42 @@ import { mapActions } from 'vuex'
import { timeFix } from '@/utils/util'
export default {
data() {
data () {
return {
loginBtn: false,
loginType: 0,
isLoginError: false,
form: this.$form.createForm(this),
state: {
time: 60,
loginBtn: false,
loginType: 0
}
form: {
username: 'admin',
password: 'daobang123'
},
rules: {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
},
loading: false
}
},
methods: {
...mapActions(['Login', 'Logout']),
handleSubmit(e) {
e.preventDefault()
const {
form: { validateFields },
state,
Login
} = this
state.loginBtn = true
const validateFieldsKey = ['mobile', 'password']
validateFields(validateFieldsKey, { force: true }, (err, values) => {
if (!err) {
const loginParams = { ...values }
delete loginParams.username
loginParams[!state.loginType ? 'email' : 'username'] = values.username
loginParams.password = values.password
Login(loginParams)
.then((res) => this.loginSuccess(res))
.catch(err => this.requestFailed(err))
.finally(() => {
state.loginBtn = false
})
handleSubmit () {
if (this.loading) {
return false
}
this.loading = false
this.$refs.ruleForm.validate(valid => {
const { form } = this
if (valid) {
this.Login(form).then(() => {
this.loginSuccess()
}, () => {
this.requestFailed()
})
} else {
setTimeout(() => {
state.loginBtn = false
}, 600)
this.loading = false
return false
}
})
},
loginSuccess(res) {
loginSuccess (res) {
this.loading = false
this.$router.push({ path: '/' })
// 1
setTimeout(() => {
@ -91,33 +67,13 @@ export default {
description: `${timeFix()},欢迎回来`
})
}, 1000)
this.isLoginError = false
},
requestFailed() {
this.isLoginError = true
requestFailed () {
this.loading = false
}
}
}
</script>
<style lang="less" scoped>
.user-layout-login {
label {
font-size: 14px;
}
.getCaptcha {
display: block;
width: 100%;
height: 40px;
}
.forge-password {
font-size: 14px;
}
button.login-button {
padding: 0 15px;
font-size: 16px;
height: 40px;
width: 100%;
}
}
</style>